MikaFil commited on
Commit
3018fcd
·
verified ·
1 Parent(s): b2fd3d9

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,13 +1,12 @@
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
- // FREE CAMERA + COLLISION ROBUSTE (type FPS léger) pour PlayCanvas (sans Ammo)
4
- // - Souris : rotation (look around)
5
- // - ZQSD / Flèches : déplacement local (avant/arrière & strafe)
6
- // - Molette / Pinch : dolly (avance/recul le long du regard)
7
- // - Collisions : sphère (caméra) vs AABBs "épaissies & fusionnées" du GLB (focusEntity)
8
- // - Déplacements "swept" (sous-division en pas) pour éviter le tunneling
9
- // - minY + BBox globale optionnelle (Xmin..Zmax) restent actives
10
- // - Conserve les noms publics "orbitCamera*" pour compatibilité avec l'existant
11
  // ============================================================================
12
 
13
  var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera"
@@ -22,24 +21,30 @@ FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title
22
  // minY (altitude min du point CAMÉRA)
23
  FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' });
24
 
25
- // Vitesse de déplacement (m/s)
26
  FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
27
  FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
28
  FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
29
 
30
  // Collision (caméra sphère)
31
  FreeCamera.attributes.add('collisionRadius', { type: 'number', default: 0.30, title: 'Camera Sphere Radius' });
32
- FreeCamera.attributes.add('collisionEpsilon', { type: 'number', default: 0.001, title: 'Collision Epsilon' });
33
 
34
- // Sous-division du déplacement (swept)
35
  FreeCamera.attributes.add('maxStepDistance', { type: 'number', default: 0.20, title: 'Max step distance (swept move)' });
36
  FreeCamera.attributes.add('maxResolveIters', { type: 'number', default: 6, title: 'Max resolve iterations per step' });
37
 
38
  // Construction des colliders
39
- FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.03, title: 'Merge AABBs gap tolerance (m)' });
40
- FreeCamera.attributes.add('inflateBias',{ type: 'number', default: 0.02, title: 'Extra inflate beyond radius (m)' });
 
 
41
 
42
- // Bounding Box globale (active si Xmin<Xmax etc.)
 
 
 
 
43
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
44
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
45
  FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
@@ -47,7 +52,7 @@ FreeCamera.attributes.add('Ymax', { type: 'number', default: Infinity, title: '
47
  FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' });
48
  FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' });
49
 
50
- // Compat (certaines non utilisées, gardées pour le viewer)
51
  FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Root (ENV GLB)' });
52
  FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
53
  FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
@@ -61,43 +66,35 @@ Object.defineProperty(FreeCamera.prototype, 'pitch', {
61
  });
62
  Object.defineProperty(FreeCamera.prototype, 'yaw', {
63
  get: function () { return this._targetYaw; },
64
- set: function (v) { this._targetYaw = v; } // yaw libre (pas de clamp)
65
  });
66
 
67
  FreeCamera.prototype.initialize = function () {
68
- // angles init depuis l'orientation actuelle
69
  var q = this.entity.getRotation();
70
- var f = new pc.Vec3();
71
- q.transformVector(pc.Vec3.FORWARD, f);
72
-
73
  this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
74
- var yawQuat = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
75
- var noYawQ = new pc.Quat().mul2(yawQuat, q);
76
- var fNoYaw = new pc.Vec3();
77
- noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
78
- this._pitch = Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
79
- this._pitch = pc.math.clamp(this._pitch, this.pitchAngleMin, this.pitchAngleMax);
80
-
81
- this._targetYaw = this._yaw;
82
- this._targetPitch = this._pitch;
83
-
84
- // Appliquer orientation immédiatement
85
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
86
 
87
- // Etat input partagé
88
  this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
89
  this.state = this.app.systems.script.app.freeCamState;
90
 
91
- // Construire colliders robustes depuis le GLB (focusEntity)
92
- this._buildRobustColliders();
93
 
94
- // S’assure que la position courante respecte collisions + minY + bbox
95
  var p = this.entity.getPosition().clone();
96
- p = this._moveSweptTo(p, p); // passe par le résolveur même si delta nul
97
  this._clampPosition(p);
98
  this.entity.setPosition(p);
99
 
100
- // Aspect ratio (comme avant)
101
  var self = this;
102
  this._onResize = function(){ self._checkAspectRatio(); };
103
  window.addEventListener('resize', this._onResize, false);
@@ -105,13 +102,13 @@ FreeCamera.prototype.initialize = function () {
105
  };
106
 
107
  FreeCamera.prototype.update = function (dt) {
108
- // Rotation inertielle
109
  var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
110
  this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
111
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
112
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
113
 
114
- // Recadrage léger
115
  var pos = this.entity.getPosition().clone();
116
  pos = this._resolveCollisions(pos, this.maxResolveIters);
117
  this._clampPosition(pos);
@@ -124,66 +121,84 @@ FreeCamera.prototype._checkAspectRatio = function () {
124
  this.entity.camera.horizontalFov = (gd.height > gd.width);
125
  };
126
 
127
- // ======================== Colliders robustes ===========================
128
- FreeCamera.prototype._buildRobustColliders = function () {
129
- this._colliders = []; // { aabb: BoundingBox }
130
- this._worldAabb = null; // BoundingBox globale du décor (utile debug / clamp)
 
131
 
132
  if (!this.focusEntity) return;
133
 
134
- // 1) Récupère toutes les AABBs world des meshInstances du GLB
135
- var tmp = [];
136
  var stack = [this.focusEntity];
137
  while (stack.length) {
138
  var e = stack.pop();
139
  var rc = e.render;
140
  if (rc && rc.meshInstances && rc.meshInstances.length) {
141
  for (var i = 0; i < rc.meshInstances.length; i++) {
142
- var aabb = rc.meshInstances[i].aabb;
143
- // clone
144
- tmp.push(new pc.BoundingBox(aabb.center.clone(), aabb.halfExtents.clone()));
145
  }
146
  }
147
  var ch = e.children;
148
  if (ch && ch.length) for (var c = 0; c < ch.length; c++) stack.push(ch[c]);
149
  }
150
-
151
- if (tmp.length === 0) return;
152
-
153
- // 2) Gonfle chaque AABB du rayon de la caméra + biais
154
- var inflate = Math.max(0, this.collisionRadius) + Math.max(0, this.inflateBias);
155
- for (var k = 0; k < tmp.length; k++) {
156
- var bb = tmp[k];
157
- bb.halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
 
 
 
 
 
 
 
 
 
158
  }
159
 
160
- // 3) Fusionne les AABBs qui se touchent/quasi-se touchent (mergeGap)
161
- var merged = this._mergeAabbs(tmp, this.mergeGap);
162
 
163
- // 4) Enregistre
 
164
  for (var m = 0; m < merged.length; m++) {
165
- this._colliders.push({ aabb: merged[m] });
166
- if (!this._worldAabb) {
167
- this._worldAabb = new pc.BoundingBox(merged[m].center.clone(), merged[m].halfExtents.clone());
168
- } else {
169
- this._worldAabb.add(merged[m]);
170
- }
 
 
 
 
 
 
 
171
  }
172
  };
173
 
174
- // Fusion simple O(n^2) jusqu’à stabilisation
175
  FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
 
176
  var out = boxes.slice();
177
  var changed = true;
178
- var g = Math.max(0, gap || 0);
179
 
180
- function overlapsOrTouches(a, b, tol) {
181
  var amin = a.getMin(), amax = a.getMax();
182
  var bmin = b.getMin(), bmax = b.getMax();
183
  return !(
184
- amax.x < bmin.x - tol || amin.x > bmax.x + tol ||
185
- amax.y < bmin.y - tol || amin.y > bmax.y + tol ||
186
- amax.z < bmin.z - tol || amin.z > bmax.z + tol
187
  );
188
  }
189
 
@@ -195,43 +210,25 @@ FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
195
  for (var i = 0; i < out.length; i++) {
196
  if (used[i]) continue;
197
  var acc = out[i];
198
- var mergedAny = false;
199
-
200
  for (var j = i + 1; j < out.length; j++) {
201
  if (used[j]) continue;
202
- if (overlapsOrTouches(acc, out[j], g)) {
203
- // fusion : créer une nouvelle AABB couvrant acc et out[j]
204
- var accMin = acc.getMin(), accMax = acc.getMax();
205
  var bMin = out[j].getMin(), bMax = out[j].getMax();
206
-
207
- var nMin = new pc.Vec3(
208
- Math.min(accMin.x, bMin.x),
209
- Math.min(accMin.y, bMin.y),
210
- Math.min(accMin.z, bMin.z)
211
- );
212
- var nMax = new pc.Vec3(
213
- Math.max(accMax.x, bMax.x),
214
- Math.max(accMax.y, bMax.y),
215
- Math.max(accMax.z, bMax.z)
216
- );
217
-
218
- var nCenter = nMin.clone().add(nMax).mulScalar(0.5);
219
- var nHalf = nMax.clone().sub(nCenter).abs();
220
- acc = new pc.BoundingBox(nCenter, nHalf);
221
-
222
  used[j] = true;
223
- mergedAny = true;
224
  changed = true;
225
  }
226
  }
227
-
228
  used[i] = true;
229
  next.push(acc);
230
  }
231
-
232
  out = next;
233
  }
234
-
235
  return out;
236
  };
237
 
@@ -241,18 +238,17 @@ FreeCamera.prototype._bboxEnabled = function () {
241
  };
242
 
243
  FreeCamera.prototype._clampPosition = function (p) {
244
- // minY prioritaire
245
  if (p.y < this.minY) p.y = this.minY;
246
  if (!this._bboxEnabled()) return;
247
-
248
  p.x = pc.math.clamp(p.x, this.Xmin, this.Xmax);
249
  p.y = pc.math.clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
250
  p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
251
  };
252
 
253
  // ======================== Mouvement swept + résolution =====================
254
- // Déplace "de -> to" par petits pas pour éviter le tunneling.
255
  FreeCamera.prototype._moveSweptTo = function (from, to) {
 
 
256
  var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
257
  var delta = to.clone().sub(from);
258
  var dist = delta.length();
@@ -274,9 +270,11 @@ FreeCamera.prototype._moveSweptTo = function (from, to) {
274
  return cur;
275
  };
276
 
277
- // Résolution itérative sphère (caméra) vs AABBs "épaissies"
278
  FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
 
 
279
  var p = pos.clone();
 
280
  var eps = Math.max(1e-7, this.collisionEpsilon);
281
 
282
  if (!this._colliders || this._colliders.length === 0) return p;
@@ -287,11 +285,9 @@ FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
287
 
288
  for (var i = 0; i < this._colliders.length; i++) {
289
  var aabb = this._colliders[i].aabb;
 
290
 
291
- // Point le plus proche de p sur l'AABB
292
- var min = aabb.getMin();
293
- var max = aabb.getMax();
294
-
295
  var cx = pc.math.clamp(p.x, min.x, max.x);
296
  var cy = pc.math.clamp(p.y, min.y, max.y);
297
  var cz = pc.math.clamp(p.z, min.z, max.z);
@@ -299,56 +295,23 @@ FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
299
  var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
300
  var distSq = dx*dx + dy*dy + dz*dz;
301
 
302
- // Comme les AABBs sont déjà "épaissies" du rayon, ici on traite une sphère ~ de rayon ~0
303
- // => il suffit de sortir du volume (point dans AABB élargie)
304
- if (distSq < eps*eps && // p quasi dedans et très proche
305
- (p.x > min.x - eps && p.x < max.x + eps &&
306
- p.y > min.y - eps && p.y < max.y + eps &&
307
- p.z > min.z - eps && p.z < max.z + eps)) {
308
-
309
- // Choisir l'axe de moindre déplacement pour sortir
310
- var ex = Math.min(Math.abs(p.x - min.x), Math.abs(max.x - p.x));
311
- var ey = Math.min(Math.abs(p.y - min.y), Math.abs(max.y - p.y));
312
- var ez = Math.min(Math.abs(p.z - min.z), Math.abs(max.z - p.z));
313
-
314
- if (ex <= ey && ex <= ez) {
315
- // pousser sur X
316
- if (Math.abs(p.x - min.x) < Math.abs(max.x - p.x)) p.x = min.x - eps; else p.x = max.x + eps;
317
- } else if (ey <= ex && ey <= ez) {
318
- // pousser sur Y (gère bord/nez de marche)
319
- 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;
320
- } else {
321
- // pousser sur Z
322
- if (Math.abs(p.z - min.z) < Math.abs(max.z - p.z)) p.z = min.z - eps; else p.z = max.z + eps;
323
- }
324
- moved = true;
325
- continue;
326
- }
327
 
328
- // Cas général : p dans l'AABB "épaissie" (ou très proche) -> projeter vers l'extérieur
329
- var inside =
330
- (p.x > min.x - eps && p.x < max.x + eps &&
331
- p.y > min.y - eps && p.y < max.y + eps &&
332
- p.z > min.z - eps && p.z < max.z + eps);
333
-
334
- if (inside) {
335
- var dxMin = Math.abs(p.x - min.x), dxMax = Math.abs(max.x - p.x);
336
- var dyMin = Math.abs(p.y - min.y), dyMax = Math.abs(max.y - p.y);
337
- var dzMin = Math.abs(p.z - min.z), dzMax = Math.abs(max.z - p.z);
338
-
339
- var ax = Math.min(dxMin, dxMax);
340
- var ay = Math.min(dyMin, dyMax);
341
- var az = Math.min(dzMin, dzMax);
342
-
343
- if (ax <= ay && ax <= az) {
344
- // sortir par X
345
- if (dxMin < dxMax) p.x = min.x - eps; else p.x = max.x + eps;
346
- } else if (ay <= ax && ay <= az) {
347
- // sortir par Y
348
- if (dyMin < dyMax) p.y = Math.max(this.minY, min.y - eps); else p.y = max.y + eps;
349
  } else {
350
- // sortir par Z
351
- if (dzMin < dzMax) p.z = min.z - eps; else p.z = max.z + eps;
 
 
 
 
 
352
  }
353
  moved = true;
354
  }
@@ -360,13 +323,13 @@ FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
360
  return p;
361
  };
362
 
363
- // ===================== INPUT SOURIS (rotation + molette dolly) =====================
364
- var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse'); // garder le nom
365
  FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' });
366
  FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
367
 
368
  FreeCameraInputMouse.prototype.initialize = function () {
369
- this.freeCam = this.entity.script.orbitCamera; // instance FreeCamera
370
  this.last = new pc.Vec2();
371
  this.isLooking = false;
372
 
@@ -392,31 +355,25 @@ FreeCameraInputMouse.prototype.initialize = function () {
392
  });
393
  };
394
 
395
- FreeCameraInputMouse.prototype.onMouseDown = function (e) {
396
- this.isLooking = true;
397
- this.last.set(e.x, e.y);
398
- };
399
-
400
- FreeCameraInputMouse.prototype.onMouseUp = function () { this.isLooking = false; };
401
 
402
  FreeCameraInputMouse.prototype.onMouseMove = function (e) {
403
  if (!this.isLooking || !this.freeCam) return;
404
- var sens = this.lookSensitivity;
405
- this.freeCam.yaw = this.freeCam.yaw - e.dx * sens;
406
- this.freeCam.pitch = this.freeCam.pitch - e.dy * sens;
407
  this.last.set(e.x, e.y);
408
  };
409
 
410
  FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
411
  if (!this.freeCam) return;
412
-
413
  var cam = this.entity;
414
  var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
415
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
416
 
417
  var from = cam.getPosition().clone();
418
  var to = from.clone().add(forward.mulScalar(move));
419
-
420
  var next = this.freeCam._moveSweptTo(from, to);
421
  this.freeCam._clampPosition(next);
422
  cam.setPosition(next);
@@ -424,10 +381,10 @@ FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
424
  e.event.preventDefault();
425
  };
426
 
427
- // ===================== INPUT TOUCH (look + pinch dolly) =====================
428
  var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
429
- FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
430
- FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
431
 
432
  FreeCameraInputTouch.prototype.initialize = function () {
433
  this.freeCam = this.entity.script.orbitCamera;
@@ -441,7 +398,6 @@ FreeCameraInputTouch.prototype.initialize = function () {
441
  this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
442
  this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
443
  }
444
-
445
  this.on('destroy', () => {
446
  if (this.app.touch) {
447
  this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
@@ -458,8 +414,7 @@ FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) {
458
  this.isLooking = (e.event.type === 'touchstart');
459
  this.last.set(t[0].x, t[0].y);
460
  } else if (t.length === 2) {
461
- var dx = t[0].x - t[1].x;
462
- var dy = t[0].y - t[1].y;
463
  this.lastPinch = Math.sqrt(dx*dx + dy*dy);
464
  } else {
465
  this.isLooking = false;
@@ -471,37 +426,29 @@ FreeCameraInputTouch.prototype.onTouchMove = function (e) {
471
  if (!this.freeCam) return;
472
 
473
  if (t.length === 1 && this.isLooking) {
474
- var sens = this.lookSensitivity;
475
- var dx = t[0].x - this.last.x;
476
- var dy = t[0].y - this.last.y;
477
-
478
- this.freeCam.yaw = this.freeCam.yaw - dx * sens;
479
- this.freeCam.pitch = this.freeCam.pitch - dy * sens;
480
-
481
  this.last.set(t[0].x, t[0].y);
482
  } else if (t.length === 2) {
483
- // Pinch dolly
484
- var dx = t[0].x - t[1].x;
485
- var dy = t[0].y - t[1].y;
486
  var dist = Math.sqrt(dx*dx + dy*dy);
487
- var delta = dist - this.lastPinch;
488
- this.lastPinch = dist;
489
 
490
  var cam = this.entity;
491
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
492
  var from = cam.getPosition().clone();
493
  var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
494
-
495
  var next = this.freeCam._moveSweptTo(from, to);
496
  this.freeCam._clampPosition(next);
497
  cam.setPosition(next);
498
  }
499
  };
500
 
501
- // ===================== INPUT CLAVIER (ZQSD + flèches) =====================
502
  var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
503
-
504
- FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused, future)' });
505
 
506
  FreeCameraInputKeyboard.prototype.initialize = function () {
507
  this.freeCam = this.entity.script.orbitCamera;
@@ -511,10 +458,8 @@ FreeCameraInputKeyboard.prototype.initialize = function () {
511
  FreeCameraInputKeyboard.prototype.update = function (dt) {
512
  if (!this.freeCam || !this.kb) return;
513
 
514
- // Déplacements : flèches OU ZQSD (AZERTY)
515
  var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
516
  (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
517
-
518
  var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 :
519
  (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q)) ? -1 : 0;
520
 
@@ -522,12 +467,12 @@ FreeCameraInputKeyboard.prototype.update = function (dt) {
522
  var cam = this.entity;
523
  var from = cam.getPosition().clone();
524
 
525
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
526
- var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize();
527
 
528
- var delta = new pc.Vec3();
529
- delta.add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt));
530
- delta.add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
531
 
532
  var to = from.clone().add(delta);
533
  var next = this.freeCam._moveSweptTo(from, to);
@@ -535,14 +480,11 @@ FreeCameraInputKeyboard.prototype.update = function (dt) {
535
  cam.setPosition(next);
536
  }
537
 
538
- // Rotation au clavier (Shift + flèches)
539
  var shift = this.kb.isPressed(pc.KEY_SHIFT);
540
  if (shift) {
541
- var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0); // ← CCW / → CW
542
- var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0); // ↑ up / ↓ down
543
-
544
- var yawSpeed = 120; // deg/s
545
- var pitchSpeed = 90; // deg/s
546
  if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
547
  if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
548
  }
 
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
+ // FREE CAMERA + COLLISION ROBUSTE (sans Ammo) version "indoor safe"
4
+ // - Souris : look
5
+ // - ZQSD/Flèches : déplacement local
6
+ // - Molette/Pinch : dolly avant/arrière
7
+ // - Collisions : sphère (caméra) vs AABBs NON fusionnées (ou très peu),
8
+ // avec filtrage des AABBs "quasi globales" (évite de bloquer toute la pièce)
9
+ // - Mouvement "swept" (sous-division) pour éviter le tunneling
 
10
  // ============================================================================
11
 
12
  var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera"
 
21
  // minY (altitude min du point CAMÉRA)
22
  FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' });
23
 
24
+ // Vitesses
25
  FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
26
  FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
27
  FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
28
 
29
  // Collision (caméra sphère)
30
  FreeCamera.attributes.add('collisionRadius', { type: 'number', default: 0.30, title: 'Camera Sphere Radius' });
31
+ FreeCamera.attributes.add('collisionEpsilon', { type: 'number', default: 0.0005, title: 'Collision Epsilon' });
32
 
33
+ // Mouvement "swept"
34
  FreeCamera.attributes.add('maxStepDistance', { type: 'number', default: 0.20, title: 'Max step distance (swept move)' });
35
  FreeCamera.attributes.add('maxResolveIters', { type: 'number', default: 6, title: 'Max resolve iterations per step' });
36
 
37
  // Construction des colliders
38
+ // IMPORTANT: garder très petit; on n'"épaissit" plus agressivement pour l'indoor
39
+ FreeCamera.attributes.add('inflateBias', { type: 'number', default: 0.0, title: 'Extra inflate (m, tiny)' });
40
+ // On ne merge presque pas : uniquement si recouvrement réel (tolérance numérique)
41
+ FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.0, title: 'Merge AABBs gap (0 = pas de gap)' });
42
 
43
+ // Filtrage "anti-coquille" : on supprime toute AABB dont la taille est trop proche de l’AABB monde
44
+ // Par axe: if halfExtents >= worldHalfExtents * (1 - globalCullFrac) => on la considère "quasi globale".
45
+ FreeCamera.attributes.add('globalCullFrac', { type: 'number', default: 0.08, title: 'Cull near-global AABBs (0.08=8%)' });
46
+
47
+ // Bounding Box globale optionnelle (Xmin..Zmax)
48
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
49
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
50
  FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
 
52
  FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' });
53
  FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' });
54
 
55
+ // Compat (gardées pour le viewer)
56
  FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Root (ENV GLB)' });
57
  FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
58
  FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
 
66
  });
67
  Object.defineProperty(FreeCamera.prototype, 'yaw', {
68
  get: function () { return this._targetYaw; },
69
+ set: function (v) { this._targetYaw = v; } // yaw libre
70
  });
71
 
72
  FreeCamera.prototype.initialize = function () {
73
+ // angles init
74
  var q = this.entity.getRotation();
75
+ var f = new pc.Vec3(); q.transformVector(pc.Vec3.FORWARD, f);
 
 
76
  this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
77
+ var yawQ = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
78
+ var noYawQ = new pc.Quat().mul2(yawQ, q);
79
+ var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
80
+ this._pitch = pc.math.clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchAngleMin, this.pitchAngleMax);
81
+ this._targetYaw = this._yaw; this._targetPitch = this._pitch;
 
 
 
 
 
 
82
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
83
 
84
+ // état partagé
85
  this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
86
  this.state = this.app.systems.script.app.freeCamState;
87
 
88
+ // colliders
89
+ this._buildIndoorSafeColliders(); // <= clé de la correction
90
 
91
+ // Forcer une passe de résolution initiale (si on spawn “dans” un mur)
92
  var p = this.entity.getPosition().clone();
93
+ p = this._moveSweptTo(p, p);
94
  this._clampPosition(p);
95
  this.entity.setPosition(p);
96
 
97
+ // aspect
98
  var self = this;
99
  this._onResize = function(){ self._checkAspectRatio(); };
100
  window.addEventListener('resize', this._onResize, false);
 
102
  };
103
 
104
  FreeCamera.prototype.update = function (dt) {
105
+ // rotation inertielle
106
  var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
107
  this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
108
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
109
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
110
 
111
+ // petit “reclamp”
112
  var pos = this.entity.getPosition().clone();
113
  pos = this._resolveCollisions(pos, this.maxResolveIters);
114
  this._clampPosition(pos);
 
121
  this.entity.camera.horizontalFov = (gd.height > gd.width);
122
  };
123
 
124
+ // ======================== Colliders indoor-safe ===========================
125
+ FreeCamera.prototype._buildIndoorSafeColliders = function () {
126
+ this._colliders = [];
127
+ this._worldAabb = null;
128
+ this._useCollision = false;
129
 
130
  if (!this.focusEntity) return;
131
 
132
+ // 1) Collecter toutes les AABBs world des meshInstances
133
+ var boxes = [];
134
  var stack = [this.focusEntity];
135
  while (stack.length) {
136
  var e = stack.pop();
137
  var rc = e.render;
138
  if (rc && rc.meshInstances && rc.meshInstances.length) {
139
  for (var i = 0; i < rc.meshInstances.length; i++) {
140
+ var a = rc.meshInstances[i].aabb;
141
+ boxes.push(new pc.BoundingBox(a.center.clone(), a.halfExtents.clone()));
 
142
  }
143
  }
144
  var ch = e.children;
145
  if (ch && ch.length) for (var c = 0; c < ch.length; c++) stack.push(ch[c]);
146
  }
147
+ if (boxes.length === 0) return;
148
+
149
+ // 2) AABB monde (non gonflée)
150
+ var world = boxes[0].clone();
151
+ for (var j = 1; j < boxes.length; j++) world.add(boxes[j]);
152
+
153
+ // 3) Filtrer les AABBs "quasi globales" (anti-coquille)
154
+ var frac = pc.math.clamp(this.globalCullFrac, 0, 0.49); // max 49% de tolérance
155
+ var wh = world.halfExtents;
156
+ var filtered = [];
157
+ for (var k = 0; k < boxes.length; k++) {
158
+ var h = boxes[k].halfExtents;
159
+ var nearGlobal =
160
+ (h.x >= wh.x * (1 - frac)) &&
161
+ (h.y >= wh.y * (1 - frac)) &&
162
+ (h.z >= wh.z * (1 - frac));
163
+ if (!nearGlobal) filtered.push(boxes[k]);
164
  }
165
 
166
+ // 4) Optionnel : petit merge *strict* (uniquement si chevauchement réel)
167
+ var merged = this._mergeAabbs(filtered, Math.max(0, this.mergeGap || 0));
168
 
169
+ // 5) Gonfler très légèrement (epsilon), pas du rayon complet — le rayon est traité par la résolution
170
+ var inflate = Math.max(0, this.inflateBias || 0);
171
  for (var m = 0; m < merged.length; m++) {
172
+ merged[m].halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
173
+ }
174
+
175
+ // 6) Enregistrer
176
+ if (merged.length > 0) {
177
+ for (var t = 0; t < merged.length; t++) this._colliders.push({ aabb: merged[t] });
178
+ this._worldAabb = world;
179
+ this._useCollision = true;
180
+ } else {
181
+ // Aucun collider utile après filtrage : on désactive les collisions pour ne pas bloquer
182
+ this._colliders = [];
183
+ this._useCollision = false;
184
+ console.warn('[orbitCamera] Aucun collider utile trouvé (désactivation des collisions)');
185
  }
186
  };
187
 
188
+ // Merge strict (gap==0 => uniquement si chevauchement)
189
  FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
190
+ if (!boxes || boxes.length <= 1) return boxes.slice();
191
  var out = boxes.slice();
192
  var changed = true;
193
+ var tol = Math.max(0, gap || 0);
194
 
195
+ function overlap(a, b, t) {
196
  var amin = a.getMin(), amax = a.getMax();
197
  var bmin = b.getMin(), bmax = b.getMax();
198
  return !(
199
+ amax.x < bmin.x - t || amin.x > bmax.x + t ||
200
+ amax.y < bmin.y - t || amin.y > bmax.y + t ||
201
+ amax.z < bmin.z - t || amin.z > bmax.z + t
202
  );
203
  }
204
 
 
210
  for (var i = 0; i < out.length; i++) {
211
  if (used[i]) continue;
212
  var acc = out[i];
 
 
213
  for (var j = i + 1; j < out.length; j++) {
214
  if (used[j]) continue;
215
+ if (overlap(acc, out[j], tol)) {
216
+ var aMin = acc.getMin(), aMax = acc.getMax();
 
217
  var bMin = out[j].getMin(), bMax = out[j].getMax();
218
+ var nMin = new pc.Vec3(Math.min(aMin.x, bMin.x), Math.min(aMin.y, bMin.y), Math.min(aMin.z, bMin.z));
219
+ var nMax = new pc.Vec3(Math.max(aMax.x, bMax.x), Math.max(aMax.y, bMax.y), Math.max(aMax.z, bMax.z));
220
+ var c = nMin.clone().add(nMax).mulScalar(0.5);
221
+ var h = nMax.clone().sub(c).abs();
222
+ acc = new pc.BoundingBox(c, h);
 
 
 
 
 
 
 
 
 
 
 
223
  used[j] = true;
 
224
  changed = true;
225
  }
226
  }
 
227
  used[i] = true;
228
  next.push(acc);
229
  }
 
230
  out = next;
231
  }
 
232
  return out;
233
  };
234
 
 
238
  };
239
 
240
  FreeCamera.prototype._clampPosition = function (p) {
 
241
  if (p.y < this.minY) p.y = this.minY;
242
  if (!this._bboxEnabled()) return;
 
243
  p.x = pc.math.clamp(p.x, this.Xmin, this.Xmax);
244
  p.y = pc.math.clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
245
  p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
246
  };
247
 
248
  // ======================== Mouvement swept + résolution =====================
 
249
  FreeCamera.prototype._moveSweptTo = function (from, to) {
250
+ if (!this._useCollision) return to.clone(); // collisions désactivées => pas de blocage
251
+
252
  var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
253
  var delta = to.clone().sub(from);
254
  var dist = delta.length();
 
270
  return cur;
271
  };
272
 
 
273
  FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
274
+ if (!this._useCollision) return pos.clone();
275
+
276
  var p = pos.clone();
277
+ var R = Math.max(0, this.collisionRadius);
278
  var eps = Math.max(1e-7, this.collisionEpsilon);
279
 
280
  if (!this._colliders || this._colliders.length === 0) return p;
 
285
 
286
  for (var i = 0; i < this._colliders.length; i++) {
287
  var aabb = this._colliders[i].aabb;
288
+ var min = aabb.getMin(); var max = aabb.getMax();
289
 
290
+ // Calcul du point le plus proche
 
 
 
291
  var cx = pc.math.clamp(p.x, min.x, max.x);
292
  var cy = pc.math.clamp(p.y, min.y, max.y);
293
  var cz = pc.math.clamp(p.z, min.z, max.z);
 
295
  var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
296
  var distSq = dx*dx + dy*dy + dz*dz;
297
 
298
+ // Collision sphère vs AABB (AABB non "gonflée" au rayon ; on le traite ici)
299
+ if (distSq < (R*R)) {
300
+ var dist = Math.sqrt(Math.max(distSq, 1e-12));
301
+ var pen = R - dist + eps;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ if (dist > 1e-6) {
304
+ p.x += (dx / dist) * pen;
305
+ p.y += (dy / dist) * pen;
306
+ p.z += (dz / dist) * pen;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  } else {
308
+ // au même point : pousse vers l'axe de plus faible sortie
309
+ var ex = Math.min(Math.abs(p.x - min.x), Math.abs(max.x - p.x));
310
+ var ey = Math.min(Math.abs(p.y - min.y), Math.abs(max.y - p.y));
311
+ var ez = Math.min(Math.abs(p.z - min.z), Math.abs(max.z - p.z));
312
+ if (ex <= ey && ex <= ez) p.x += (Math.abs(p.x - min.x) < Math.abs(max.x - p.x) ? -pen : pen);
313
+ else if (ey <= ex && ey <= ez) p.y += (Math.abs(p.y - min.y) < Math.abs(max.y - p.y) ? -pen : pen);
314
+ else p.z += (Math.abs(p.z - min.z) < Math.abs(max.z - p.z) ? -pen : pen);
315
  }
316
  moved = true;
317
  }
 
323
  return p;
324
  };
325
 
326
+ // ===================== INPUT SOURIS =====================
327
+ var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse');
328
  FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' });
329
  FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
330
 
331
  FreeCameraInputMouse.prototype.initialize = function () {
332
+ this.freeCam = this.entity.script.orbitCamera;
333
  this.last = new pc.Vec2();
334
  this.isLooking = false;
335
 
 
355
  });
356
  };
357
 
358
+ FreeCameraInputMouse.prototype.onMouseDown = function (e) { this.isLooking = true; this.last.set(e.x, e.y); };
359
+ FreeCameraInputMouse.prototype.onMouseUp = function () { this.isLooking = false; };
 
 
 
 
360
 
361
  FreeCameraInputMouse.prototype.onMouseMove = function (e) {
362
  if (!this.isLooking || !this.freeCam) return;
363
+ var s = this.lookSensitivity;
364
+ this.freeCam.yaw = this.freeCam.yaw - e.dx * s;
365
+ this.freeCam.pitch = this.freeCam.pitch - e.dy * s;
366
  this.last.set(e.x, e.y);
367
  };
368
 
369
  FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
370
  if (!this.freeCam) return;
 
371
  var cam = this.entity;
372
  var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
373
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
374
 
375
  var from = cam.getPosition().clone();
376
  var to = from.clone().add(forward.mulScalar(move));
 
377
  var next = this.freeCam._moveSweptTo(from, to);
378
  this.freeCam._clampPosition(next);
379
  cam.setPosition(next);
 
381
  e.event.preventDefault();
382
  };
383
 
384
+ // ===================== INPUT TOUCH =====================
385
  var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
386
+ FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
387
+ FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
388
 
389
  FreeCameraInputTouch.prototype.initialize = function () {
390
  this.freeCam = this.entity.script.orbitCamera;
 
398
  this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
399
  this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
400
  }
 
401
  this.on('destroy', () => {
402
  if (this.app.touch) {
403
  this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
 
414
  this.isLooking = (e.event.type === 'touchstart');
415
  this.last.set(t[0].x, t[0].y);
416
  } else if (t.length === 2) {
417
+ var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
 
418
  this.lastPinch = Math.sqrt(dx*dx + dy*dy);
419
  } else {
420
  this.isLooking = false;
 
426
  if (!this.freeCam) return;
427
 
428
  if (t.length === 1 && this.isLooking) {
429
+ var s = this.lookSensitivity;
430
+ var dx = t[0].x - this.last.x, dy = t[0].y - this.last.y;
431
+ this.freeCam.yaw = this.freeCam.yaw - dx * s;
432
+ this.freeCam.pitch = this.freeCam.pitch - dy * s;
 
 
 
433
  this.last.set(t[0].x, t[0].y);
434
  } else if (t.length === 2) {
435
+ var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
 
 
436
  var dist = Math.sqrt(dx*dx + dy*dy);
437
+ var delta = dist - this.lastPinch; this.lastPinch = dist;
 
438
 
439
  var cam = this.entity;
440
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
441
  var from = cam.getPosition().clone();
442
  var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
 
443
  var next = this.freeCam._moveSweptTo(from, to);
444
  this.freeCam._clampPosition(next);
445
  cam.setPosition(next);
446
  }
447
  };
448
 
449
+ // ===================== INPUT CLAVIER =====================
450
  var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
451
+ FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused)' });
 
452
 
453
  FreeCameraInputKeyboard.prototype.initialize = function () {
454
  this.freeCam = this.entity.script.orbitCamera;
 
458
  FreeCameraInputKeyboard.prototype.update = function (dt) {
459
  if (!this.freeCam || !this.kb) return;
460
 
 
461
  var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
462
  (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
 
463
  var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 :
464
  (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q)) ? -1 : 0;
465
 
 
467
  var cam = this.entity;
468
  var from = cam.getPosition().clone();
469
 
470
+ var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
471
+ var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize();
472
 
473
+ var delta = new pc.Vec3()
474
+ .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
475
+ .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
476
 
477
  var to = from.clone().add(delta);
478
  var next = this.freeCam._moveSweptTo(from, to);
 
480
  cam.setPosition(next);
481
  }
482
 
 
483
  var shift = this.kb.isPressed(pc.KEY_SHIFT);
484
  if (shift) {
485
+ var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0);
486
+ var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0);
487
+ var yawSpeed = 120, pitchSpeed = 90;
 
 
488
  if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
489
  if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
490
  }