viewer_sgos / deplacement_dans_env /ctrl_camera_pr_env.js
MikaFil's picture
Update deplacement_dans_env/ctrl_camera_pr_env.js
646e744 verified
raw
history blame
23.3 kB
// ctrl_camera_pr_env.js
// ============================================================================
// FREE CAMERA + COLLISION ROBUSTE (type FPS léger) pour PlayCanvas (sans Ammo)
// - Souris : rotation (look around)
// - ZQSD / Flèches : déplacement local (avant/arrière & strafe)
// - Molette / Pinch : dolly (avance/recul le long du regard)
// - Collisions : sphère (caméra) vs AABBs "épaissies & fusionnées" du GLB (focusEntity)
// - Déplacements "swept" (sous-division en pas) pour éviter le tunneling
// - minY + BBox globale optionnelle (Xmin..Zmax) restent actives
// - Conserve les noms publics "orbitCamera*" pour compatibilité avec l'existant
// ============================================================================
var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera"
// ======================== Attributs ===========================
FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.12, title: 'Inertia (rotation)' });
// Limites de pitch
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.2, title: 'Move Speed' });
FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
// Collision (caméra sphère)
FreeCamera.attributes.add('collisionRadius', { type: 'number', default: 0.30, title: 'Camera Sphere Radius' });
FreeCamera.attributes.add('collisionEpsilon', { type: 'number', default: 0.001, title: 'Collision Epsilon' });
// Sous-division du déplacement (swept)
FreeCamera.attributes.add('maxStepDistance', { type: 'number', default: 0.20, title: 'Max step distance (swept move)' });
FreeCamera.attributes.add('maxResolveIters', { type: 'number', default: 6, title: 'Max resolve iterations per step' });
// Construction des colliders
FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.03, title: 'Merge AABBs gap tolerance (m)' });
FreeCamera.attributes.add('inflateBias',{ type: 'number', default: 0.02, title: 'Extra inflate beyond radius (m)' });
// Bounding Box globale (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 (certaines non utilisées, gardées pour le viewer)
FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Root (ENV GLB)' });
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);
this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
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é
this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
this.state = this.app.systems.script.app.freeCamState;
// Construire colliders robustes depuis le GLB (focusEntity)
this._buildRobustColliders();
// S’assure que la position courante respecte collisions + minY + bbox
var p = this.entity.getPosition().clone();
p = this._moveSweptTo(p, p); // passe par le résolveur même si delta nul
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);
// Recadrage léger
var pos = this.entity.getPosition().clone();
pos = this._resolveCollisions(pos, this.maxResolveIters);
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);
};
// ======================== Colliders robustes ===========================
FreeCamera.prototype._buildRobustColliders = function () {
this._colliders = []; // { aabb: BoundingBox }
this._worldAabb = null; // BoundingBox globale du décor (utile debug / clamp)
if (!this.focusEntity) return;
// 1) Récupère toutes les AABBs world des meshInstances du GLB
var tmp = [];
var stack = [this.focusEntity];
while (stack.length) {
var e = stack.pop();
var rc = e.render;
if (rc && rc.meshInstances && rc.meshInstances.length) {
for (var i = 0; i < rc.meshInstances.length; i++) {
var aabb = rc.meshInstances[i].aabb;
// clone
tmp.push(new pc.BoundingBox(aabb.center.clone(), aabb.halfExtents.clone()));
}
}
var ch = e.children;
if (ch && ch.length) for (var c = 0; c < ch.length; c++) stack.push(ch[c]);
}
if (tmp.length === 0) return;
// 2) Gonfle chaque AABB du rayon de la caméra + biais
var inflate = Math.max(0, this.collisionRadius) + Math.max(0, this.inflateBias);
for (var k = 0; k < tmp.length; k++) {
var bb = tmp[k];
bb.halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
}
// 3) Fusionne les AABBs qui se touchent/quasi-se touchent (mergeGap)
var merged = this._mergeAabbs(tmp, this.mergeGap);
// 4) Enregistre
for (var m = 0; m < merged.length; m++) {
this._colliders.push({ aabb: merged[m] });
if (!this._worldAabb) {
this._worldAabb = new pc.BoundingBox(merged[m].center.clone(), merged[m].halfExtents.clone());
} else {
this._worldAabb.add(merged[m]);
}
}
};
// Fusion simple O(n^2) jusqu’à stabilisation
FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
var out = boxes.slice();
var changed = true;
var g = Math.max(0, gap || 0);
function overlapsOrTouches(a, b, tol) {
var amin = a.getMin(), amax = a.getMax();
var bmin = b.getMin(), bmax = b.getMax();
return !(
amax.x < bmin.x - tol || amin.x > bmax.x + tol ||
amax.y < bmin.y - tol || amin.y > bmax.y + tol ||
amax.z < bmin.z - tol || amin.z > bmax.z + tol
);
}
while (changed) {
changed = false;
var next = [];
var used = new Array(out.length).fill(false);
for (var i = 0; i < out.length; i++) {
if (used[i]) continue;
var acc = out[i];
var mergedAny = false;
for (var j = i + 1; j < out.length; j++) {
if (used[j]) continue;
if (overlapsOrTouches(acc, out[j], g)) {
// fusion : créer une nouvelle AABB couvrant acc et out[j]
var accMin = acc.getMin(), accMax = acc.getMax();
var bMin = out[j].getMin(), bMax = out[j].getMax();
var nMin = new pc.Vec3(
Math.min(accMin.x, bMin.x),
Math.min(accMin.y, bMin.y),
Math.min(accMin.z, bMin.z)
);
var nMax = new pc.Vec3(
Math.max(accMax.x, bMax.x),
Math.max(accMax.y, bMax.y),
Math.max(accMax.z, bMax.z)
);
var nCenter = nMin.clone().add(nMax).mulScalar(0.5);
var nHalf = nMax.clone().sub(nCenter).abs();
acc = new pc.BoundingBox(nCenter, nHalf);
used[j] = true;
mergedAny = true;
changed = true;
}
}
used[i] = true;
next.push(acc);
}
out = next;
}
return out;
};
// ======================== Contraintes génériques ===========================
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);
};
// ======================== Mouvement swept + résolution =====================
// Déplace "de -> to" par petits pas pour éviter le tunneling.
FreeCamera.prototype._moveSweptTo = function (from, to) {
var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
var delta = to.clone().sub(from);
var dist = delta.length();
if (dist <= maxStep) {
var p = from.clone().add(delta);
p = this._resolveCollisions(p, this.maxResolveIters);
return p;
}
var steps = Math.ceil(dist / maxStep);
var stepVec = delta.divScalar(steps);
var cur = from.clone();
for (var i = 0; i < steps; i++) {
cur.add(stepVec);
cur = this._resolveCollisions(cur, this.maxResolveIters);
}
return cur;
};
// Résolution itérative sphère (caméra) vs AABBs "épaissies"
FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
var p = pos.clone();
var eps = Math.max(1e-7, this.collisionEpsilon);
if (!this._colliders || this._colliders.length === 0) return p;
var iters = Math.max(1, maxIters || 1);
for (var iter = 0; iter < iters; iter++) {
var moved = false;
for (var i = 0; i < this._colliders.length; i++) {
var aabb = this._colliders[i].aabb;
// Point le plus proche de p sur l'AABB
var min = aabb.getMin();
var max = aabb.getMax();
var cx = pc.math.clamp(p.x, min.x, max.x);
var cy = pc.math.clamp(p.y, min.y, max.y);
var cz = pc.math.clamp(p.z, min.z, max.z);
var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
var distSq = dx*dx + dy*dy + dz*dz;
// Comme les AABBs sont déjà "épaissies" du rayon, ici on traite une sphère ~ de rayon ~0
// => il suffit de sortir du volume (point dans AABB élargie)
if (distSq < eps*eps && // p quasi dedans et très proche
(p.x > min.x - eps && p.x < max.x + eps &&
p.y > min.y - eps && p.y < max.y + eps &&
p.z > min.z - eps && p.z < max.z + eps)) {
// Choisir l'axe de moindre déplacement pour sortir
var ex = Math.min(Math.abs(p.x - min.x), Math.abs(max.x - p.x));
var ey = Math.min(Math.abs(p.y - min.y), Math.abs(max.y - p.y));
var ez = Math.min(Math.abs(p.z - min.z), Math.abs(max.z - p.z));
if (ex <= ey && ex <= ez) {
// pousser sur X
if (Math.abs(p.x - min.x) < Math.abs(max.x - p.x)) p.x = min.x - eps; else p.x = max.x + eps;
} else if (ey <= ex && ey <= ez) {
// pousser sur Y (gère bord/nez de marche)
if (Math.abs(p.y - min.y) < Math.abs(max.y - p.y)) p.y = Math.max(this.minY, min.y - eps); else p.y = max.y + eps;
} else {
// pousser sur Z
if (Math.abs(p.z - min.z) < Math.abs(max.z - p.z)) p.z = min.z - eps; else p.z = max.z + eps;
}
moved = true;
continue;
}
// Cas général : p dans l'AABB "épaissie" (ou très proche) -> projeter vers l'extérieur
var inside =
(p.x > min.x - eps && p.x < max.x + eps &&
p.y > min.y - eps && p.y < max.y + eps &&
p.z > min.z - eps && p.z < max.z + eps);
if (inside) {
var dxMin = Math.abs(p.x - min.x), dxMax = Math.abs(max.x - p.x);
var dyMin = Math.abs(p.y - min.y), dyMax = Math.abs(max.y - p.y);
var dzMin = Math.abs(p.z - min.z), dzMax = Math.abs(max.z - p.z);
var ax = Math.min(dxMin, dxMax);
var ay = Math.min(dyMin, dyMax);
var az = Math.min(dzMin, dzMax);
if (ax <= ay && ax <= az) {
// sortir par X
if (dxMin < dxMax) p.x = min.x - eps; else p.x = max.x + eps;
} else if (ay <= ax && ay <= az) {
// sortir par Y
if (dyMin < dyMax) p.y = Math.max(this.minY, min.y - eps); else p.y = max.y + eps;
} else {
// sortir par Z
if (dzMin < dzMax) p.z = min.z - eps; else p.z = max.z + eps;
}
moved = true;
}
}
if (!moved) break;
}
return p;
};
// ===================== 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) {
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;
this.freeCam.yaw = this.freeCam.yaw - e.dx * sens;
this.freeCam.pitch = this.freeCam.pitch - e.dy * sens;
this.last.set(e.x, e.y);
};
FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
if (!this.freeCam) return;
var cam = this.entity;
var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
var from = cam.getPosition().clone();
var to = from.clone().add(forward.mulScalar(move));
var next = this.freeCam._moveSweptTo(from, to);
this.freeCam._clampPosition(next);
cam.setPosition(next);
e.event.preventDefault();
};
// ===================== INPUT TOUCH (look + pinch dolly) =====================
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(); if (forward.lengthSq() > 1e-8) forward.normalize();
var from = cam.getPosition().clone();
var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
var next = this.freeCam._moveSweptTo(from, to);
this.freeCam._clampPosition(next);
cam.setPosition(next);
}
};
// ===================== 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 from = 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();
var delta = new pc.Vec3();
delta.add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt));
delta.add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
var to = from.clone().add(delta);
var next = this.freeCam._moveSweptTo(from, to);
this.freeCam._clampPosition(next);
cam.setPosition(next);
}
// Rotation au clavier (Shift + flèches)
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
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;
}
};