MikaFil commited on
Commit
2200173
·
verified ·
1 Parent(s): 455b989

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,18 +1,27 @@
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
- // FREE CAMERA + COLLISION ROBUSTE (sans Ammo) version "indoor safe"
4
- // Respecte la position initiale (JSON). Ne spawne à l’intérieur que si nécessaire.
 
 
 
 
 
5
  // ============================================================================
6
 
7
- var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera"
8
 
9
  // ======================== Attributs ===========================
10
  FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.12, title: 'Inertia (rotation)' });
 
 
11
  FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
12
  FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
13
 
 
14
  FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' });
15
 
 
16
  FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
17
  FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
18
  FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse/Pinch Dolly Speed' });
@@ -25,15 +34,21 @@ FreeCamera.attributes.add('collisionEpsilon', { type: 'number', default: 0.0005,
25
  FreeCamera.attributes.add('maxStepDistance', { type: 'number', default: 0.20, title: 'Max step distance (swept move)' });
26
  FreeCamera.attributes.add('maxResolveIters', { type: 'number', default: 6, title: 'Max resolve iterations per step' });
27
 
28
- // Construction des colliders (indoor-safe)
29
- FreeCamera.attributes.add('inflateBias', { type: 'number', default: 0.0, title: 'Extra inflate (m, tiny)' });
30
- FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.0, title: 'Merge AABBs gap (0 = pas de gap)' });
31
- FreeCamera.attributes.add('globalCullFrac', { type: 'number', default: 0.08, title: 'Cull near-global AABBs (0.08=8%)' });
 
 
 
 
 
 
32
 
33
- // Spawn auto (seulement si la position initiale est invalide)
34
- FreeCamera.attributes.add('autoSpawnInside', { type: 'boolean', default: false, title: 'Auto-spawn inside if needed' });
35
 
36
- // Bounding Box globale optionnelle (Xmin..Zmax)
37
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
38
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
39
  FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
@@ -48,6 +63,18 @@ FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title
48
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
49
  FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
50
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  // ======================== Initialisation ===========================
52
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
53
  get: function () { return this._targetPitch; },
@@ -77,13 +104,12 @@ FreeCamera.prototype.initialize = function () {
77
  // colliders
78
  this._buildIndoorSafeColliders();
79
 
80
- // ======== respecter la position initiale, ne spawner dedans que si nécessaire ========
81
  if (this.autoSpawnInside && this._useCollision) {
82
  var start = this.entity.getPosition().clone();
83
  if (this._shouldAutoSpawn(start)) {
84
- this._spawnInside(); // place au centre + pose sur sol
85
  } else {
86
- // juste assainir si on spawn légèrement intersecté
87
  var safe = this._resolveCollisions(start, this.maxResolveIters);
88
  this._clampPosition(safe);
89
  this.entity.setPosition(safe);
@@ -104,7 +130,7 @@ FreeCamera.prototype.update = function (dt) {
104
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
105
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
106
 
107
- // petit “reclamp”
108
  var pos = this.entity.getPosition().clone();
109
  pos = this._resolveCollisions(pos, this.maxResolveIters);
110
  this._clampPosition(pos);
@@ -180,7 +206,7 @@ FreeCamera.prototype._buildIndoorSafeColliders = function () {
180
  }
181
  };
182
 
183
- // Merge strict
184
  FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
185
  if (!boxes || boxes.length <= 1) return boxes.slice();
186
  var out = boxes.slice();
@@ -241,8 +267,7 @@ FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
241
  return out;
242
  };
243
 
244
- // ======================== SPAWN INTÉRIEUR (optionnel) ===========================
245
- // décide si on doit "forcer" un spawn intérieur
246
  FreeCamera.prototype._shouldAutoSpawn = function (pos) {
247
  if (!this._useCollision || !this._worldAabb) return false;
248
 
@@ -252,20 +277,20 @@ FreeCamera.prototype._shouldAutoSpawn = function (pos) {
252
  var wmin = this._worldAabb.getMin();
253
  var wmax = this._worldAabb.getMax();
254
 
255
- // 1) hors monde (avec marge du rayon)
256
  if (pos.x < wmin.x + R + eps || pos.x > wmax.x - R - eps) return true;
257
  if (pos.z < wmin.z + R + eps || pos.z > wmax.z - R - eps) return true;
258
  if (pos.y < wmin.y - eps || pos.y > wmax.y + eps) return true;
259
 
260
- // 2) pas de "sol" sous ce XZ
261
  var bestY = this._highestYUnderXZ(pos.x, pos.z, eps);
262
  if (bestY === -Infinity) return true;
263
 
264
- // 3) si on est très en dessous du sol (ou bien au-dessus du plafond)
265
  if (pos.y < bestY - 2.0) return true;
266
  if (pos.y > wmax.y + 0.5) return true;
267
 
268
- // 4) si on intersecte déjà fortement un collider (la résolution bouge beaucoup)
269
  var resolved = this._resolveCollisions(pos, this.maxResolveIters);
270
  var move = resolved.clone().sub(pos).length();
271
  if (move > R * 0.5) return true;
@@ -273,7 +298,6 @@ FreeCamera.prototype._shouldAutoSpawn = function (pos) {
273
  return false;
274
  };
275
 
276
- // calcule le maxY des AABBs couvrant (x,z) — "sol" approximé
277
  FreeCamera.prototype._highestYUnderXZ = function (x, z, eps) {
278
  var bestY = -Infinity;
279
  if (!this._colliders) return bestY;
@@ -287,7 +311,6 @@ FreeCamera.prototype._highestYUnderXZ = function (x, z, eps) {
287
  return bestY;
288
  };
289
 
290
- // place la caméra à l’intérieur: centre XZ, “drop” sur le sol/marche sous ce XZ
291
  FreeCamera.prototype._spawnInside = function () {
292
  if (!this._useCollision || !this._worldAabb) return;
293
 
@@ -323,31 +346,143 @@ FreeCamera.prototype._clampPosition = function (p) {
323
  p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
324
  };
325
 
326
- // ======================== Mouvement swept + résolution =====================
327
- FreeCamera.prototype._moveSweptTo = function (from, to) {
328
- if (!this._useCollision) return to.clone();
 
329
 
 
330
  var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
331
- var delta = to.clone().sub(from);
332
- var dist = delta.length();
333
-
334
- if (dist <= maxStep) {
335
- var p = from.clone().add(delta);
336
- p = this._resolveCollisions(p, this.maxResolveIters);
337
- return p;
338
- }
339
 
340
  var steps = Math.ceil(dist / maxStep);
341
- var stepVec = delta.divScalar(steps);
342
  var cur = from.clone();
343
-
344
  for (var i = 0; i < steps; i++) {
345
- cur.add(stepVec);
346
- cur = this._resolveCollisions(cur, this.maxResolveIters);
347
  }
348
  return cur;
349
  };
350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
352
  if (!this._useCollision) return pos.clone();
353
 
@@ -451,7 +586,7 @@ FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
451
 
452
  var from = cam.getPosition().clone();
453
  var to = from.clone().add(forward.mulScalar(move));
454
- var next = this.freeCam._moveSweptTo(from, to);
455
  this.freeCam._clampPosition(next);
456
  cam.setPosition(next);
457
 
@@ -517,7 +652,7 @@ FreeCameraInputTouch.prototype.onTouchMove = function (e) {
517
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
518
  var from = cam.getPosition().clone();
519
  var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
520
- var next = this.freeCam._moveSweptTo(from, to);
521
  this.freeCam._clampPosition(next);
522
  cam.setPosition(next);
523
  }
@@ -551,8 +686,7 @@ FreeCameraInputKeyboard.prototype.update = function (dt) {
551
  .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
552
  .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
553
 
554
- var to = from.clone().add(delta);
555
- var next = this.freeCam._moveSweptTo(from, to);
556
  this.freeCam._clampPosition(next);
557
  cam.setPosition(next);
558
  }
 
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
+ // FREE CAMERA + COLLISION INDOOR-SAFE (sans Ammo) avec STEP OFFSET (escaliers)
4
+ // - Souris : look
5
+ // - ZQSD / Flèches : déplacement local
6
+ // - Molette / Pinch : dolly
7
+ // - Collisions : sphère (caméra) vs AABBs du GLB (focusEntity)
8
+ // - Mouvement "swept" + slide sur murs + step-up/step-down pour escaliers
9
+ // - Placement initial : respecte la config; auto-spawn inside seulement si nécessaire
10
  // ============================================================================
11
 
12
+ var FreeCamera = pc.createScript('orbitCamera'); // garder ce nom pour compat viewer
13
 
14
  // ======================== Attributs ===========================
15
  FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.12, title: 'Inertia (rotation)' });
16
+
17
+ // Limites de pitch
18
  FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
19
  FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
20
 
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' });
 
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
+ // Indoor-safe colliders
38
+ FreeCamera.attributes.add('inflateBias', { type: 'number', default: 0.0, title: 'Extra inflate (m, tiny)' });
39
+ FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.0, title: 'Merge AABBs gap (0 = chevauchement réel seulement)' });
40
+ FreeCamera.attributes.add('globalCullFrac',{ type: 'number', default: 0.08, title: 'Cull near-global AABBs (0.08=8%)' });
41
+
42
+ // Step offset / Ground snap (escaliers)
43
+ FreeCamera.attributes.add('stepHeight', { type: 'number', default: 0.35, title: 'Max step-up height (m)' });
44
+ FreeCamera.attributes.add('stepAhead', { type: 'number', default: 0.20, title: 'Probe distance ahead for step (m)' });
45
+ FreeCamera.attributes.add('stepDownMax', { type: 'number', default: 0.60, title: 'Max snap-down (m)' });
46
+ FreeCamera.attributes.add('enableGroundSnap', { type: 'boolean', default: true, title: 'Enable ground snap' });
47
 
48
+ // Spawn auto si nécessaire seulement
49
+ FreeCamera.attributes.add('autoSpawnInside', { type: 'boolean', default: true, title: 'Auto-spawn inside if needed' });
50
 
51
+ // BBox globale optionnelle (Xmin..Zmax)
52
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
53
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
54
  FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
 
63
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
64
  FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
65
 
66
+ // ======================== Helpers internes ===========================
67
+ function vec3Sub(a, b) { return new pc.Vec3(a.x-b.x, a.y-b.y, a.z-b.z); }
68
+ function vec3Add(a, b) { return new pc.Vec3(a.x+b.x, a.y+b.y, a.z+b.z); }
69
+ function vec3Scale(a, s){ return new pc.Vec3(a.x*s, a.y*s, a.z*s); }
70
+ function length(v) { return Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); }
71
+ function dot(a, b){ return a.x*b.x + a.y*b.y + a.z*b.z; }
72
+ function normalize(v){ var l=length(v)||1; return new pc.Vec3(v.x/l, v.y/l, v.z/l); }
73
+ function projectOnPlane(v, n){ // enlève la composante selon n
74
+ var d = dot(v, n);
75
+ return new pc.Vec3(v.x - d*n.x, v.y - d*n.y, v.z - d*n.z);
76
+ }
77
+
78
  // ======================== Initialisation ===========================
79
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
80
  get: function () { return this._targetPitch; },
 
104
  // colliders
105
  this._buildIndoorSafeColliders();
106
 
107
+ // Respecter la position initiale; ne spawner dedans que si nécessaire
108
  if (this.autoSpawnInside && this._useCollision) {
109
  var start = this.entity.getPosition().clone();
110
  if (this._shouldAutoSpawn(start)) {
111
+ this._spawnInside();
112
  } else {
 
113
  var safe = this._resolveCollisions(start, this.maxResolveIters);
114
  this._clampPosition(safe);
115
  this.entity.setPosition(safe);
 
130
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
131
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
132
 
133
+ // maintien propre (au cas où)
134
  var pos = this.entity.getPosition().clone();
135
  pos = this._resolveCollisions(pos, this.maxResolveIters);
136
  this._clampPosition(pos);
 
206
  }
207
  };
208
 
209
+ // Merge strict (gap==0 => chevauchement seulement)
210
  FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
211
  if (!boxes || boxes.length <= 1) return boxes.slice();
212
  var out = boxes.slice();
 
267
  return out;
268
  };
269
 
270
+ // ======================== SPAWN / VALIDATION POSITION ===========================
 
271
  FreeCamera.prototype._shouldAutoSpawn = function (pos) {
272
  if (!this._useCollision || !this._worldAabb) return false;
273
 
 
277
  var wmin = this._worldAabb.getMin();
278
  var wmax = this._worldAabb.getMax();
279
 
280
+ // hors monde (avec marge du rayon)
281
  if (pos.x < wmin.x + R + eps || pos.x > wmax.x - R - eps) return true;
282
  if (pos.z < wmin.z + R + eps || pos.z > wmax.z - R - eps) return true;
283
  if (pos.y < wmin.y - eps || pos.y > wmax.y + eps) return true;
284
 
285
+ // pas de "sol" sous ce XZ
286
  var bestY = this._highestYUnderXZ(pos.x, pos.z, eps);
287
  if (bestY === -Infinity) return true;
288
 
289
+ // trop en dessous/au dessus
290
  if (pos.y < bestY - 2.0) return true;
291
  if (pos.y > wmax.y + 0.5) return true;
292
 
293
+ // forte intersection initiale
294
  var resolved = this._resolveCollisions(pos, this.maxResolveIters);
295
  var move = resolved.clone().sub(pos).length();
296
  if (move > R * 0.5) return true;
 
298
  return false;
299
  };
300
 
 
301
  FreeCamera.prototype._highestYUnderXZ = function (x, z, eps) {
302
  var bestY = -Infinity;
303
  if (!this._colliders) return bestY;
 
311
  return bestY;
312
  };
313
 
 
314
  FreeCamera.prototype._spawnInside = function () {
315
  if (!this._useCollision || !this._worldAabb) return;
316
 
 
346
  p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
347
  };
348
 
349
+ // ======================== MOUVEMENT PRINCIPAL ===========================
350
+ FreeCamera.prototype._moveWithCollisions = function (from, desiredDelta) {
351
+ // 1) tentative pure sans collision si désactivées
352
+ if (!this._useCollision) return from.clone().add(desiredDelta);
353
 
354
+ // 2) swept: divise en petits pas
355
  var maxStep = Math.max(0.01, this.maxStepDistance || 0.2);
356
+ var total = desiredDelta.clone();
357
+ var dist = length(total);
358
+ if (dist <= maxStep) return this._moveStep(from, total);
 
 
 
 
 
359
 
360
  var steps = Math.ceil(dist / maxStep);
361
+ var stepVec = vec3Scale(total, 1/steps);
362
  var cur = from.clone();
 
363
  for (var i = 0; i < steps; i++) {
364
+ cur = this._moveStep(cur, stepVec);
 
365
  }
366
  return cur;
367
  };
368
 
369
+ // Un "pas" unique avec slide + step-up + snap-down
370
+ FreeCamera.prototype._moveStep = function (from, delta) {
371
+ var target = vec3Add(from, delta);
372
+
373
+ // a) tentative simple
374
+ var after = this._resolveCollisions(target, this.maxResolveIters);
375
+ var movedVec = vec3Sub(after, target);
376
+ var movedLen = length(movedVec);
377
+
378
+ // si aucune collision, on snap-down optionnel
379
+ if (movedLen === 0) {
380
+ if (this.enableGroundSnap) after = this._snapDown(after);
381
+ return after;
382
+ }
383
+
384
+ // b) collision détectée → slide (supprime la composante dans la normale estimée)
385
+ var n = this._estimateCollisionNormal(after); // approx par rapport aux AABB proches
386
+ if (n) {
387
+ var desire = delta.clone();
388
+ var slideDelta = projectOnPlane(desire, n);
389
+ var slideTarget = vec3Add(from, slideDelta);
390
+ var slideAfter = this._resolveCollisions(slideTarget, this.maxResolveIters);
391
+
392
+ // c) Step-up si le slide ne suffit pas (test petite marche)
393
+ if (length(vec3Sub(slideAfter, slideTarget)) !== 0) {
394
+ var stepped = this._tryStepUp(from, desire);
395
+ if (stepped) {
396
+ if (this.enableGroundSnap) stepped = this._snapDown(stepped);
397
+ return stepped;
398
+ }
399
+ }
400
+
401
+ if (this.enableGroundSnap) slideAfter = this._snapDown(slideAfter);
402
+ return slideAfter;
403
+ }
404
+
405
+ // pas de normale estimable (rare) : accepte la position résolue
406
+ if (this.enableGroundSnap) after = this._snapDown(after);
407
+ return after;
408
+ };
409
+
410
+ // Estimation grossière d’une normale de collision autour de "p"
411
+ FreeCamera.prototype._estimateCollisionNormal = function (p) {
412
+ if (!this._colliders) return null;
413
+ var probe = 0.02;
414
+ var base = this._resolveCollisions(p, this.maxResolveIters);
415
+ var nx = this._resolveCollisions(new pc.Vec3(p.x+probe, p.y, p.z), this.maxResolveIters);
416
+ var px = this._resolveCollisions(new pc.Vec3(p.x-probe, p.y, p.z), this.maxResolveIters);
417
+ var ny = this._resolveCollisions(new pc.Vec3(p.x, p.y+probe, p.z), this.maxResolveIters);
418
+ var py = this._resolveCollisions(new pc.Vec3(p.x, p.y-probe, p.z), this.maxResolveIters);
419
+ var nz = this._resolveCollisions(new pc.Vec3(p.x, p.y, p.z+probe), this.maxResolveIters);
420
+ var pz = this._resolveCollisions(new pc.Vec3(p.x, p.y, p.z-probe), this.maxResolveIters);
421
+
422
+ var dx = length(vec3Sub(nx, base)) - length(vec3Sub(px, base));
423
+ var dy = length(vec3Sub(ny, base)) - length(vec3Sub(py, base));
424
+ var dz = length(vec3Sub(nz, base)) - length(vec3Sub(pz, base));
425
+ var n = new pc.Vec3(dx, dy, dz);
426
+ var L = length(n);
427
+ if (L < 1e-5) return null;
428
+ return vec3Scale(n, 1/L);
429
+ };
430
+
431
+ // Step-up : essaie de monter une marche jusqu’à stepHeight
432
+ FreeCamera.prototype._tryStepUp = function (from, wishDelta) {
433
+ var R = Math.max(0, this.collisionRadius);
434
+ var eps = Math.max(1e-4, this.collisionEpsilon);
435
+ var maxH = Math.max(0, this.stepHeight || 0.35);
436
+ var ahead = Math.max(0.05, this.stepAhead || 0.20);
437
+
438
+ // direction de déplacement horizontal
439
+ var horiz = new pc.Vec3(wishDelta.x, 0, wishDelta.z);
440
+ var hLen = length(horiz);
441
+ if (hLen < 1e-6) return null;
442
+ horiz = vec3Scale(horiz, 1/hLen);
443
+
444
+ // point de test un peu devant
445
+ var probeXZ = vec3Add(from, vec3Scale(horiz, Math.min(hLen, ahead)));
446
+ var bestY = this._highestYUnderXZ(probeXZ.x, probeXZ.z, eps);
447
+
448
+ if (bestY === -Infinity) return null; // rien pour se poser
449
+
450
+ // si la marche est à maxH au-dessus de la position actuelle → tenter un step
451
+ var curY = from.y;
452
+ var desiredY = bestY + R + 0.02; // poser sur la marche
453
+ var up = desiredY - curY;
454
+
455
+ if (up > 0 && up <= maxH + 1e-4) {
456
+ // tenter : monter puis appliquer le delta complet
457
+ var raised = new pc.Vec3(from.x, curY + up, from.z);
458
+ raised = this._resolveCollisions(raised, this.maxResolveIters);
459
+ var stepped = this._resolveCollisions(vec3Add(raised, wishDelta), this.maxResolveIters);
460
+ // valider si on a bien avancé
461
+ if (length(vec3Sub(stepped, raised)) > 0.01) return stepped;
462
+ }
463
+
464
+ return null;
465
+ };
466
+
467
+ // Ground snap : colle vers le sol si on est en l’air (descente d’escaliers)
468
+ FreeCamera.prototype._snapDown = function (p) {
469
+ var R = Math.max(0, this.collisionRadius);
470
+ var eps = Math.max(1e-4, this.collisionEpsilon);
471
+ var maxDown = Math.max(0, this.stepDownMax || 0.6);
472
+
473
+ var bestY = this._highestYUnderXZ(p.x, p.z, eps);
474
+ if (bestY === -Infinity) return p;
475
+
476
+ var floorY = bestY + R + 0.01;
477
+ if (p.y - floorY > 0 && p.y - floorY <= maxDown) {
478
+ var snapped = new pc.Vec3(p.x, floorY, p.z);
479
+ return this._resolveCollisions(snapped, this.maxResolveIters);
480
+ }
481
+ return p;
482
+ };
483
+
484
+ // ======================== Résolution collision sphère/AABB =====================
485
+ // Déplace et “expulse” si p (centre sphère) intersecte un AABB.
486
  FreeCamera.prototype._resolveCollisions = function (pos, maxIters) {
487
  if (!this._useCollision) return pos.clone();
488
 
 
586
 
587
  var from = cam.getPosition().clone();
588
  var to = from.clone().add(forward.mulScalar(move));
589
+ var next = this.freeCam._moveWithCollisions(from, to.sub(from));
590
  this.freeCam._clampPosition(next);
591
  cam.setPosition(next);
592
 
 
652
  var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
653
  var from = cam.getPosition().clone();
654
  var to = from.clone().add(forward.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
655
+ var next = this.freeCam._moveWithCollisions(from, to.sub(from));
656
  this.freeCam._clampPosition(next);
657
  cam.setPosition(next);
658
  }
 
686
  .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
687
  .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
688
 
689
+ var next = this.freeCam._moveWithCollisions(from, delta);
 
690
  this.freeCam._clampPosition(next);
691
  cam.setPosition(next);
692
  }