Spaces:
Running
Running
| // ctrl_camera_pr_env.js | |
| // ============================================================================ | |
| // FREE CAMERA (sans orbite) pour PlayCanvas | |
| // - Souris : rotation sur place (comme Shift+flèches auparavant) | |
| // - ZQSD / Flèches : déplacement local (avant/arrière & strafe) | |
| // - Molette : dolly (avance/recul le long du regard) | |
| // - Bounding Box + minY : contraintes sur la POSITION caméra uniquement | |
| // - AUCUNE logique d’orbite (pas de pivot, pas de distance d’orbite) | |
| // ============================================================================ | |
| var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera" pour compat viewer | |
| // ======================== Attributs =========================== | |
| FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.12, title: 'Inertia (rotation)' }); | |
| // Limites de pitch (conseil: -89..89 si vue libre) | |
| FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' }); | |
| FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' }); | |
| // minY (altitude min du point CAMÉRA) | |
| FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' }); | |
| // Vitesse de déplacement (m/s) | |
| FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.0, title: 'Move Speed' }); | |
| FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.0, title: 'Strafe Speed' }); | |
| FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse Wheel Dolly Speed' }); | |
| // Bounding Box (active si Xmin<Xmax etc.) | |
| FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' }); | |
| FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' }); | |
| FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' }); | |
| FreeCamera.attributes.add('Ymax', { type: 'number', default: Infinity, title: 'BBox Ymax' }); | |
| FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' }); | |
| FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' }); | |
| // Compat (ignorés mais laissés pour ne pas casser le viewer existant) | |
| FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Compat: Focus Entity (unused)' }); | |
| FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' }); | |
| FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' }); | |
| FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' }); | |
| FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' }); | |
| // ======================== Initialisation =========================== | |
| Object.defineProperty(FreeCamera.prototype, 'pitch', { | |
| get: function () { return this._targetPitch; }, | |
| set: function (v) { this._targetPitch = pc.math.clamp(v, this.pitchAngleMin, this.pitchAngleMax); } | |
| }); | |
| Object.defineProperty(FreeCamera.prototype, 'yaw', { | |
| get: function () { return this._targetYaw; }, | |
| set: function (v) { this._targetYaw = v; } // yaw libre (pas de clamp) | |
| }); | |
| FreeCamera.prototype.initialize = function () { | |
| // angles init depuis l'orientation actuelle | |
| var q = this.entity.getRotation(); | |
| var f = new pc.Vec3(); | |
| q.transformVector(pc.Vec3.FORWARD, f); | |
| // Convention PlayCanvas: setLocalEulerAngles(pitch, yaw, 0) | |
| this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG; | |
| // retirer le yaw pour extraire un pitch cohérent | |
| var yawQuat = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0); | |
| var noYawQ = new pc.Quat().mul2(yawQuat, q); | |
| var fNoYaw = new pc.Vec3(); | |
| noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw); | |
| this._pitch = Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG; | |
| this._pitch = pc.math.clamp(this._pitch, this.pitchAngleMin, this.pitchAngleMax); | |
| this._targetYaw = this._yaw; | |
| this._targetPitch = this._pitch; | |
| // Appliquer orientation immédiatement | |
| this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0); | |
| // Etat input (partagé par scripts d’input) | |
| this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {}; | |
| this.state = this.app.systems.script.app.freeCamState; // pointeur commun | |
| // S’assure que la position courante respecte minY + bbox | |
| var p = this.entity.getPosition().clone(); | |
| this._clampPosition(p); | |
| this.entity.setPosition(p); | |
| // Aspect ratio (comme avant) | |
| var self = this; | |
| this._onResize = function(){ self._checkAspectRatio(); }; | |
| window.addEventListener('resize', this._onResize, false); | |
| this._checkAspectRatio(); | |
| }; | |
| FreeCamera.prototype.update = function (dt) { | |
| // Rotation inertielle | |
| var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1); | |
| this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t); | |
| this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t); | |
| this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0); | |
| // Déplacements clavier (gérés dans Keyboard, mais on reclampe chaque frame) | |
| var pos = this.entity.getPosition().clone(); | |
| this._clampPosition(pos); | |
| this.entity.setPosition(pos); | |
| }; | |
| FreeCamera.prototype._checkAspectRatio = function () { | |
| var gd = this.app.graphicsDevice; | |
| if (!gd) return; | |
| this.entity.camera.horizontalFov = (gd.height > gd.width); | |
| }; | |
| // ======================== Contraintes =========================== | |
| FreeCamera.prototype._bboxEnabled = function () { | |
| return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax); | |
| }; | |
| FreeCamera.prototype._clampPosition = function (p) { | |
| // minY prioritaire | |
| if (p.y < this.minY) p.y = this.minY; | |
| if (!this._bboxEnabled()) return; | |
| p.x = pc.math.clamp(p.x, this.Xmin, this.Xmax); | |
| p.y = pc.math.clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax); | |
| p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax); | |
| }; | |
| // ===================== INPUT SOURIS (rotation + molette dolly) ===================== | |
| var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse'); // garder le nom | |
| FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' }); | |
| FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' }); | |
| FreeCameraInputMouse.prototype.initialize = function () { | |
| this.freeCam = this.entity.script.orbitCamera; // instance FreeCamera | |
| this.last = new pc.Vec2(); | |
| this.isLooking = false; | |
| if (this.app.mouse) { | |
| this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); | |
| this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this); | |
| this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this); | |
| this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this); | |
| this.app.mouse.disableContextMenu(); | |
| } | |
| var self = this; | |
| this._onOut = function(){ self.isLooking = false; }; | |
| window.addEventListener('mouseout', this._onOut, false); | |
| this.on('destroy', () => { | |
| if (this.app.mouse) { | |
| this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this); | |
| this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this); | |
| this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this); | |
| this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this); | |
| } | |
| window.removeEventListener('mouseout', this._onOut, false); | |
| }); | |
| }; | |
| FreeCameraInputMouse.prototype.onMouseDown = function (e) { | |
| // Bouton gauche/milieu/droit -> tous = "look" | |
| this.isLooking = true; | |
| this.last.set(e.x, e.y); | |
| }; | |
| FreeCameraInputMouse.prototype.onMouseUp = function () { | |
| this.isLooking = false; | |
| }; | |
| FreeCameraInputMouse.prototype.onMouseMove = function (e) { | |
| if (!this.isLooking || !this.freeCam) return; | |
| var sens = this.lookSensitivity; | |
| // Souris = même logique que "Shift + flèches" (rotation sur place) | |
| var deltaYaw = (e.dx) * sens; // droite => +dx => on veut TURN RIGHT (CW) => yaw diminue | |
| var deltaPitch = (e.dy) * sens; // haut => dy<0 => look up => pitch augmente => pitch -= deltaPitch | |
| this.freeCam.yaw = this.freeCam.yaw - deltaYaw; // yaw libre | |
| this.freeCam.pitch = this.freeCam.pitch - deltaPitch; // pitch clampé | |
| this.last.set(e.x, e.y); | |
| }; | |
| FreeCameraInputMouse.prototype.onMouseWheel = function (e) { | |
| if (!this.freeCam) return; | |
| // Dolly : avancer/reculer dans l'axe du regard | |
| var cam = this.entity; | |
| var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05; // échelle douce | |
| var forward = cam.forward.clone().normalize().mulScalar(move); | |
| var pos = cam.getPosition().clone().add(forward); | |
| this.freeCam._clampPosition(pos); | |
| cam.setPosition(pos); | |
| e.event.preventDefault(); | |
| }; | |
| // ===================== INPUT TOUCH (optionnel : look + pinch dolly simple) ===================== | |
| var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch'); | |
| FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' }); | |
| FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' }); | |
| FreeCameraInputTouch.prototype.initialize = function () { | |
| this.freeCam = this.entity.script.orbitCamera; | |
| this.last = new pc.Vec2(); | |
| this.isLooking = false; | |
| this.lastPinch = 0; | |
| if (this.app.touch) { | |
| this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this); | |
| this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this); | |
| this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this); | |
| this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this); | |
| } | |
| this.on('destroy', () => { | |
| if (this.app.touch) { | |
| this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this); | |
| this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this); | |
| this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this); | |
| this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this); | |
| } | |
| }); | |
| }; | |
| FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) { | |
| var t = e.touches; | |
| if (t.length === 1) { | |
| this.isLooking = (e.event.type === 'touchstart'); | |
| this.last.set(t[0].x, t[0].y); | |
| } else if (t.length === 2) { | |
| var dx = t[0].x - t[1].x; | |
| var dy = t[0].y - t[1].y; | |
| this.lastPinch = Math.sqrt(dx*dx + dy*dy); | |
| } else { | |
| this.isLooking = false; | |
| } | |
| }; | |
| FreeCameraInputTouch.prototype.onTouchMove = function (e) { | |
| var t = e.touches; | |
| if (!this.freeCam) return; | |
| if (t.length === 1 && this.isLooking) { | |
| var sens = this.lookSensitivity; | |
| var dx = t[0].x - this.last.x; | |
| var dy = t[0].y - this.last.y; | |
| this.freeCam.yaw = this.freeCam.yaw - dx * sens; | |
| this.freeCam.pitch = this.freeCam.pitch - dy * sens; | |
| this.last.set(t[0].x, t[0].y); | |
| } else if (t.length === 2) { | |
| // Pinch dolly | |
| var dx = t[0].x - t[1].x; | |
| var dy = t[0].y - t[1].y; | |
| var dist = Math.sqrt(dx*dx + dy*dy); | |
| var delta = dist - this.lastPinch; | |
| this.lastPinch = dist; | |
| var cam = this.entity; | |
| var forward = cam.forward.clone().normalize().mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed); | |
| var pos = cam.getPosition().clone().add(forward); | |
| this.freeCam._clampPosition(pos); | |
| cam.setPosition(pos); | |
| } | |
| }; | |
| // ===================== INPUT CLAVIER (ZQSD + flèches) ===================== | |
| var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard'); | |
| FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused, future)' }); | |
| FreeCameraInputKeyboard.prototype.initialize = function () { | |
| this.freeCam = this.entity.script.orbitCamera; | |
| this.kb = this.app.keyboard || null; | |
| }; | |
| FreeCameraInputKeyboard.prototype.update = function (dt) { | |
| if (!this.freeCam || !this.kb) return; | |
| // Déplacements : flèches OU ZQSD (AZERTY) | |
| var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 : | |
| (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0; | |
| var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 : | |
| (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q)) ? -1 : 0; | |
| if (fwd !== 0 || strf !== 0) { | |
| var cam = this.entity; | |
| var pos = cam.getPosition().clone(); | |
| var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize(); | |
| var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize(); | |
| pos.add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt)); | |
| pos.add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt)); | |
| this.freeCam._clampPosition(pos); | |
| cam.setPosition(pos); | |
| } | |
| // Rotation au clavier (Shift + flèches) — conservée | |
| var shift = this.kb.isPressed(pc.KEY_SHIFT); | |
| if (shift) { | |
| var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0); // ← CCW / → CW | |
| var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0); // ↑ up / ↓ down | |
| // Même logique que la souris : rotation sur place | |
| var yawSpeed = 120; // deg/s | |
| var pitchSpeed = 90; // deg/s | |
| if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt; | |
| if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt; | |
| } | |
| }; | |