MikaFil commited on
Commit
646e744
·
verified ·
1 Parent(s): 989bad7

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,13 +1,13 @@
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
- // FREE CAMERA + COLLISION STATIQUE (type FPS léger) pour PlayCanvas
4
- // - Souris : rotation sur place
5
  // - ZQSD / Flèches : déplacement local (avant/arrière & strafe)
6
- // - Molette : dolly (avance/recul le long du regard)
7
- // - "focusEntity" sert de RACINE de collision (typiquement : ton envEntity GLB)
8
- // - Collision sphère (caméra) vs AABBs des meshInstances statiques du GLB
9
- // - minY + Bounding Box globale (Xmin..Zmax) restent actives (post-résolution)
10
- // - Pas de physique Ammo.js requise
11
  // ============================================================================
12
 
13
  var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera"
@@ -23,13 +23,21 @@ FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title
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.0, title: 'Move Speed' });
27
- FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.0, title: 'Strafe Speed' });
28
- FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse Wheel Dolly Speed' });
29
 
30
- // Collision (caméra sphère) — rayon et marge numérique
31
- FreeCamera.attributes.add('collisionRadius', { type: 'number', default: 0.25, title: 'Camera Sphere Radius' });
32
- FreeCamera.attributes.add('collisionEpsilon', { type: 'number', default: 0.0005, title: 'Collision Epsilon' });
 
 
 
 
 
 
 
 
33
 
34
  // Bounding Box globale (active si Xmin<Xmax etc.)
35
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
@@ -44,7 +52,7 @@ FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Ro
44
  FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
45
  FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
46
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
47
- FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
48
 
49
  // ======================== Initialisation ===========================
50
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
@@ -76,27 +84,24 @@ FreeCamera.prototype.initialize = function () {
76
  // Appliquer orientation immédiatement
77
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
78
 
79
- // Etat input partagé (conservé)
80
  this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
81
  this.state = this.app.systems.script.app.freeCamState;
82
 
83
- // Contrôles de fenêtre
84
- var self = this;
85
- this._onResize = function(){ self._checkAspectRatio(); };
86
- window.addEventListener('resize', this._onResize, false);
87
- this._checkAspectRatio();
88
-
89
- // ====== COLLISIONS: construire la liste d'AABBs statiques à partir du focusEntity (ENV GLB) ======
90
- this._colliders = []; // tableau d'objets { aabb: pc.BoundingBox }
91
- if (this.focusEntity) {
92
- this._buildStaticCollidersFromRoot(this.focusEntity);
93
- }
94
 
95
- // S’assure que la position courante respecte minY + bbox + collision (si déjà dans un mur)
96
  var p = this.entity.getPosition().clone();
97
- p = this._resolveCollisions(p, /*maxIters*/ 4);
98
  this._clampPosition(p);
99
  this.entity.setPosition(p);
 
 
 
 
 
 
100
  };
101
 
102
  FreeCamera.prototype.update = function (dt) {
@@ -106,10 +111,9 @@ FreeCamera.prototype.update = function (dt) {
106
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
107
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
108
 
109
- // Reclamp soft chaque frame (utile si minY / bbox)
110
  var pos = this.entity.getPosition().clone();
111
- // En cas de dérive numérique, resserrer contre les colliders
112
- pos = this._resolveCollisions(pos, /*maxIters*/ 1);
113
  this._clampPosition(pos);
114
  this.entity.setPosition(pos);
115
  };
@@ -120,36 +124,118 @@ FreeCamera.prototype._checkAspectRatio = function () {
120
  this.entity.camera.horizontalFov = (gd.height > gd.width);
121
  };
122
 
123
- // ======================== Gestion des colliders (AABBs) ===========================
124
- FreeCamera.prototype._buildStaticCollidersFromRoot = function (root) {
125
- var stack = [root];
126
- var tempAabb = new pc.BoundingBox();
 
 
 
 
 
 
127
  while (stack.length) {
128
  var e = stack.pop();
129
-
130
- // RenderComponent => récupérer les AABBs des meshInstances
131
  var rc = e.render;
132
  if (rc && rc.meshInstances && rc.meshInstances.length) {
133
  for (var i = 0; i < rc.meshInstances.length; i++) {
134
- var mi = rc.meshInstances[i];
135
- // s'assurer que l'AABB world est à jour (PlayCanvas le met à jour à la phase de culling,
136
- // on le recalcule une fois via "getWorldTransform" si besoin)
137
- var aabb = mi.aabb;
138
- // Copier l'AABB (pour le figer) : center & halfExtents
139
- var aabbCopy = new pc.BoundingBox(aabb.center.clone(), aabb.halfExtents.clone());
140
- this._colliders.push({ aabb: aabbCopy });
141
  }
142
  }
143
-
144
- // Parcours enfants
145
  var ch = e.children;
146
- if (ch && ch.length) {
147
- for (var c = 0; c < ch.length; c++) stack.push(ch[c]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }
 
 
149
  }
 
 
150
  };
151
 
152
- // ======================== Contraintes ===========================
153
  FreeCamera.prototype._bboxEnabled = function () {
154
  return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
155
  };
@@ -164,71 +250,114 @@ FreeCamera.prototype._clampPosition = function (p) {
164
  p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
165
  };
166
 
167
- // ======================== Résolution collisions sphère vs AABB ====================
168
- // Renvoie une position corrigée en repoussant la sphère de la caméra hors des AABBs.
169
- // Simple "projection" itérative (plusieurs itérations pour empiler les corrections).
170
- FreeCamera.prototype._resolveCollisions = function (desiredPos, maxIters) {
171
- var pos = desiredPos.clone();
172
- var R = Math.max(0, this.collisionRadius);
173
- var eps = Math.max(1e-7, this.collisionEpsilon);
 
 
 
 
 
174
 
175
- if (!this._colliders || this._colliders.length === 0 || R === 0) {
176
- return pos;
 
 
 
 
 
177
  }
 
 
 
 
 
 
 
 
 
178
 
179
- for (var iter = 0; iter < (maxIters || 1); iter++) {
 
180
  var moved = false;
181
 
182
  for (var i = 0; i < this._colliders.length; i++) {
183
  var aabb = this._colliders[i].aabb;
184
 
185
- // Trouver le point le plus proche sur l'AABB par rapport au centre de la sphère
186
  var min = aabb.getMin();
187
  var max = aabb.getMax();
188
 
189
- var cx = pc.math.clamp(pos.x, min.x, max.x);
190
- var cy = pc.math.clamp(pos.y, min.y, max.y);
191
- var cz = pc.math.clamp(pos.z, min.z, max.z);
192
-
193
- var dx = pos.x - cx;
194
- var dy = pos.y - cy;
195
- var dz = pos.z - cz;
196
 
 
197
  var distSq = dx*dx + dy*dy + dz*dz;
198
 
199
- if (distSq < (R*R - eps)) {
200
- // Collision : repousser la sphère en dehors
201
- var dist = Math.sqrt(Math.max(distSq, 1e-12));
202
- var pen = R - dist;
203
-
204
- if (dist > 1e-6) {
205
- // direction = depuis le point le plus proche vers le centre
206
- pos.x += (dx / dist) * (pen + eps);
207
- pos.y += (dy / dist) * (pen + eps);
208
- pos.z += (dz / dist) * (pen + eps);
209
- moved = true;
 
 
 
 
 
 
 
210
  } else {
211
- // sphère centre exactement sur une arête/coin : pousser le long de l'axe de moindre pénétration
212
- var ex = Math.min(Math.abs(pos.x - min.x), Math.abs(max.x - pos.x));
213
- var ey = Math.min(Math.abs(pos.y - min.y), Math.abs(max.y - pos.y));
214
- var ez = Math.min(Math.abs(pos.z - min.z), Math.abs(max.z - pos.z));
215
-
216
- if (ex <= ey && ex <= ez) {
217
- pos.x += (pos.x - aabb.center.x >= 0 ? 1 : -1) * (pen + eps);
218
- } else if (ey <= ex && ey <= ez) {
219
- pos.y += (pos.y - aabb.center.y >= 0 ? 1 : -1) * (pen + eps);
220
- } else {
221
- pos.z += (pos.z - aabb.center.z >= 0 ? 1 : -1) * (pen + eps);
222
- }
223
- moved = true;
224
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  }
226
  }
227
 
228
  if (!moved) break;
229
  }
230
 
231
- return pos;
232
  };
233
 
234
  // ===================== INPUT SOURIS (rotation + molette dolly) =====================
@@ -268,38 +397,29 @@ FreeCameraInputMouse.prototype.onMouseDown = function (e) {
268
  this.last.set(e.x, e.y);
269
  };
270
 
271
- FreeCameraInputMouse.prototype.onMouseUp = function () {
272
- this.isLooking = false;
273
- };
274
 
275
  FreeCameraInputMouse.prototype.onMouseMove = function (e) {
276
  if (!this.isLooking || !this.freeCam) return;
277
  var sens = this.lookSensitivity;
278
-
279
- var deltaYaw = (e.dx) * sens;
280
- var deltaPitch = (e.dy) * sens;
281
-
282
- this.freeCam.yaw = this.freeCam.yaw - deltaYaw;
283
- this.freeCam.pitch = this.freeCam.pitch - deltaPitch;
284
-
285
  this.last.set(e.x, e.y);
286
  };
287
 
288
  FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
289
  if (!this.freeCam) return;
290
 
291
- // Dolly : avancer/reculer dans l'axe du regard
292
  var cam = this.entity;
293
- var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05; // échelle douce
294
- var forward = cam.forward.clone().normalize().mulScalar(move);
295
 
296
- var pos = cam.getPosition().clone().add(forward);
 
297
 
298
- // ===== collision + contraintes =====
299
- pos = this.freeCam._resolveCollisions(pos, /*maxIters*/ 3);
300
- this.freeCam._clampPosition(pos);
301
-
302
- cam.setPosition(pos);
303
 
304
  e.event.preventDefault();
305
  };
@@ -368,14 +488,13 @@ FreeCameraInputTouch.prototype.onTouchMove = function (e) {
368
  this.lastPinch = dist;
369
 
370
  var cam = this.entity;
371
- var forward = cam.forward.clone().normalize().mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed);
372
- var pos = cam.getPosition().clone().add(forward);
373
-
374
- // ===== collision + contraintes =====
375
- pos = this.freeCam._resolveCollisions(pos, /*maxIters*/ 3);
376
- this.freeCam._clampPosition(pos);
377
 
378
- cam.setPosition(pos);
 
 
379
  }
380
  };
381
 
@@ -401,19 +520,19 @@ FreeCameraInputKeyboard.prototype.update = function (dt) {
401
 
402
  if (fwd !== 0 || strf !== 0) {
403
  var cam = this.entity;
404
- var pos = cam.getPosition().clone();
405
 
406
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
407
  var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize();
408
 
409
- pos.add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt));
410
- pos.add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
411
-
412
- // ===== collision + contraintes =====
413
- pos = this.freeCam._resolveCollisions(pos, /*maxIters*/ 3);
414
- this.freeCam._clampPosition(pos);
415
 
416
- cam.setPosition(pos);
 
 
 
417
  }
418
 
419
  // Rotation au clavier (Shift + flèches)
 
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"
 
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' });
 
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)' });
54
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
55
+ FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
56
 
57
  // ======================== Initialisation ===========================
58
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
 
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);
104
+ this._checkAspectRatio();
105
  };
106
 
107
  FreeCamera.prototype.update = function (dt) {
 
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);
118
  this.entity.setPosition(pos);
119
  };
 
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
+
190
+ while (changed) {
191
+ changed = false;
192
+ var next = [];
193
+ var used = new Array(out.length).fill(false);
194
+
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
 
238
+ // ======================== Contraintes génériques ===========================
239
  FreeCamera.prototype._bboxEnabled = function () {
240
  return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
241
  };
 
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();
259
+
260
+ if (dist <= maxStep) {
261
+ var p = from.clone().add(delta);
262
+ p = this._resolveCollisions(p, this.maxResolveIters);
263
+ return p;
264
+ }
265
 
266
+ var steps = Math.ceil(dist / maxStep);
267
+ var stepVec = delta.divScalar(steps);
268
+ var cur = from.clone();
269
+
270
+ for (var i = 0; i < steps; i++) {
271
+ cur.add(stepVec);
272
+ cur = this._resolveCollisions(cur, this.maxResolveIters);
273
  }
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;
283
 
284
+ var iters = Math.max(1, maxIters || 1);
285
+ for (var iter = 0; iter < iters; iter++) {
286
  var moved = false;
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);
 
 
 
 
298
 
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
  }
355
  }
356
 
357
  if (!moved) break;
358
  }
359
 
360
+ return p;
361
  };
362
 
363
  // ===================== INPUT SOURIS (rotation + molette dolly) =====================
 
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);
 
 
423
 
424
  e.event.preventDefault();
425
  };
 
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
 
 
520
 
521
  if (fwd !== 0 || strf !== 0) {
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);
534
+ this.freeCam._clampPosition(next);
535
+ cam.setPosition(next);
536
  }
537
 
538
  // Rotation au clavier (Shift + flèches)