MikaFil commited on
Commit
c8d535e
·
verified ·
1 Parent(s): e4957a0

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,31 +1,51 @@
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
- // FREE CAMERA (sans orbite) pour PlayCanvas
4
- // - Souris : rotation sur place (comme Shift+flèches auparavant)
5
  // - ZQSD / Flèches : déplacement local (avant/arrière & strafe)
6
- // - Molette : dolly (avance/recul le long du regard)
7
- // - Bounding Box + minY : contraintes sur la POSITION caméra uniquement
8
- // - AUCUNE logique d’orbite (pas de pivot, pas de distance d’orbite)
 
9
  // ============================================================================
10
 
11
- var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public "orbitCamera" pour compat viewer
12
 
13
  // ======================== Attributs ===========================
14
- FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.12, title: 'Inertia (rotation)' });
15
-
16
- // Limites de pitch (conseil: -89..89 si vue libre)
17
  FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
18
  FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
19
 
20
- // minY (altitude min du point CAMÉRA)
21
- FreeCamera.attributes.add('minY', { type: 'number', default: 0, title: 'Minimum camera Y' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- // Vitesse de déplacement (m/s)
24
- FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.0, title: 'Move Speed' });
25
- FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.0, title: 'Strafe Speed' });
26
- FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.0, title: 'Mouse Wheel Dolly Speed' });
 
27
 
28
- // Bounding Box (active si Xmin<Xmax etc.)
 
 
 
 
 
29
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
30
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
31
  FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
@@ -33,55 +53,126 @@ FreeCamera.attributes.add('Ymax', { type: 'number', default: Infinity, title: '
33
  FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' });
34
  FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' });
35
 
36
- // Compat (ignorés mais laissés pour ne pas casser le viewer existant)
37
- FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Compat: Focus Entity (unused)' });
38
  FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
39
  FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
40
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
41
- FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- // ======================== Initialisation ===========================
44
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
45
  get: function () { return this._targetPitch; },
46
  set: function (v) { this._targetPitch = pc.math.clamp(v, this.pitchAngleMin, this.pitchAngleMax); }
47
  });
48
  Object.defineProperty(FreeCamera.prototype, 'yaw', {
49
  get: function () { return this._targetYaw; },
50
- set: function (v) { this._targetYaw = v; } // yaw libre (pas de clamp)
51
  });
52
 
 
53
  FreeCamera.prototype.initialize = function () {
54
- // angles init depuis l'orientation actuelle
55
  var q = this.entity.getRotation();
56
- var f = new pc.Vec3();
57
- q.transformVector(pc.Vec3.FORWARD, f);
58
 
59
- // Convention PlayCanvas: setLocalEulerAngles(pitch, yaw, 0)
60
- this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
61
- // retirer le yaw pour extraire un pitch cohérent
62
  var yawQuat = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
63
  var noYawQ = new pc.Quat().mul2(yawQuat, q);
64
- var fNoYaw = new pc.Vec3();
65
- noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
66
- this._pitch = Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG;
67
- this._pitch = pc.math.clamp(this._pitch, this.pitchAngleMin, this.pitchAngleMax);
68
 
69
- this._targetYaw = this._yaw;
70
  this._targetPitch = this._pitch;
71
-
72
- // Appliquer orientation immédiatement
73
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
74
 
75
- // Etat input (partagé par scripts d’input)
76
  this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
77
- this.state = this.app.systems.script.app.freeCamState; // pointeur commun
78
-
79
- // S’assure que la position courante respecte minY + bbox
80
- var p = this.entity.getPosition().clone();
81
- this._clampPosition(p);
82
- this.entity.setPosition(p);
 
 
 
 
 
 
 
 
 
83
 
84
- // Aspect ratio (comme avant)
85
  var self = this;
86
  this._onResize = function(){ self._checkAspectRatio(); };
87
  window.addEventListener('resize', this._onResize, false);
@@ -89,15 +180,16 @@ FreeCamera.prototype.initialize = function () {
89
  };
90
 
91
  FreeCamera.prototype.update = function (dt) {
92
- // Rotation inertielle
93
  var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
94
  this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
95
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
96
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
97
 
98
- // Déplacements clavier (gérés dans Keyboard, mais on reclampe chaque frame)
99
  var pos = this.entity.getPosition().clone();
100
- this._clampPosition(pos);
 
101
  this.entity.setPosition(pos);
102
  };
103
 
@@ -107,28 +199,278 @@ FreeCamera.prototype._checkAspectRatio = function () {
107
  this.entity.camera.horizontalFov = (gd.height > gd.width);
108
  };
109
 
110
- // ======================== Contraintes ===========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  FreeCamera.prototype._bboxEnabled = function () {
112
  return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
113
  };
114
-
115
- FreeCamera.prototype._clampPosition = function (p) {
116
- // minY prioritaire
117
  if (p.y < this.minY) p.y = this.minY;
118
  if (!this._bboxEnabled()) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- p.x = pc.math.clamp(p.x, this.Xmin, this.Xmax);
121
- p.y = pc.math.clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
122
- p.z = pc.math.clamp(p.z, this.Zmin, this.Zmax);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  };
124
 
125
- // ===================== INPUT SOURIS (rotation + molette dolly) =====================
126
- var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse'); // garder le nom
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' });
128
  FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
129
 
130
  FreeCameraInputMouse.prototype.initialize = function () {
131
- this.freeCam = this.entity.script.orbitCamera; // instance FreeCamera
132
  this.last = new pc.Vec2();
133
  this.isLooking = false;
134
 
@@ -155,7 +497,6 @@ FreeCameraInputMouse.prototype.initialize = function () {
155
  };
156
 
157
  FreeCameraInputMouse.prototype.onMouseDown = function (e) {
158
- // Bouton gauche/milieu/droit -> tous = "look"
159
  this.isLooking = true;
160
  this.last.set(e.x, e.y);
161
  };
@@ -167,36 +508,30 @@ FreeCameraInputMouse.prototype.onMouseUp = function () {
167
  FreeCameraInputMouse.prototype.onMouseMove = function (e) {
168
  if (!this.isLooking || !this.freeCam) return;
169
  var sens = this.lookSensitivity;
170
-
171
- // Souris = même logique que "Shift + flèches" (rotation sur place)
172
- var deltaYaw = (e.dx) * sens; // droite => +dx => on veut TURN RIGHT (CW) => yaw diminue
173
- var deltaPitch = (e.dy) * sens; // haut => dy<0 => look up => pitch augmente => pitch -= deltaPitch
174
-
175
- this.freeCam.yaw = this.freeCam.yaw - deltaYaw; // yaw libre
176
- this.freeCam.pitch = this.freeCam.pitch - deltaPitch; // pitch clampé
177
-
178
  this.last.set(e.x, e.y);
179
  };
180
 
181
  FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
182
  if (!this.freeCam) return;
183
-
184
- // Dolly : avancer/reculer dans l'axe du regard
185
  var cam = this.entity;
186
- var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05; // échelle douce
187
- var forward = cam.forward.clone().normalize().mulScalar(move);
188
 
189
- var pos = cam.getPosition().clone().add(forward);
190
- this.freeCam._clampPosition(pos);
191
- cam.setPosition(pos);
 
 
192
 
193
  e.event.preventDefault();
194
  };
195
 
196
- // ===================== INPUT TOUCH (optionnel : look + pinch dolly simple) =====================
197
  var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
198
- FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
199
- FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
200
 
201
  FreeCameraInputTouch.prototype.initialize = function () {
202
  this.freeCam = this.entity.script.orbitCamera;
@@ -227,8 +562,7 @@ FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) {
227
  this.isLooking = (e.event.type === 'touchstart');
228
  this.last.set(t[0].x, t[0].y);
229
  } else if (t.length === 2) {
230
- var dx = t[0].x - t[1].x;
231
- var dy = t[0].y - t[1].y;
232
  this.lastPinch = Math.sqrt(dx*dx + dy*dy);
233
  } else {
234
  this.isLooking = false;
@@ -240,33 +574,28 @@ FreeCameraInputTouch.prototype.onTouchMove = function (e) {
240
  if (!this.freeCam) return;
241
 
242
  if (t.length === 1 && this.isLooking) {
243
- var sens = this.lookSensitivity;
244
- var dx = t[0].x - this.last.x;
245
- var dy = t[0].y - this.last.y;
246
-
247
- this.freeCam.yaw = this.freeCam.yaw - dx * sens;
248
- this.freeCam.pitch = this.freeCam.pitch - dy * sens;
249
-
250
  this.last.set(t[0].x, t[0].y);
251
  } else if (t.length === 2) {
252
- // Pinch dolly
253
- var dx = t[0].x - t[1].x;
254
- var dy = t[0].y - t[1].y;
255
  var dist = Math.sqrt(dx*dx + dy*dy);
256
- var delta = dist - this.lastPinch;
257
- this.lastPinch = dist;
258
 
259
  var cam = this.entity;
260
- var forward = cam.forward.clone().normalize().mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed);
261
- var pos = cam.getPosition().clone().add(forward);
262
- this.freeCam._clampPosition(pos);
263
- cam.setPosition(pos);
 
 
264
  }
265
  };
266
 
267
- // ===================== INPUT CLAVIER (ZQSD + flèches) =====================
268
  var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
269
-
270
  FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused, future)' });
271
 
272
  FreeCameraInputKeyboard.prototype.initialize = function () {
@@ -277,7 +606,6 @@ FreeCameraInputKeyboard.prototype.initialize = function () {
277
  FreeCameraInputKeyboard.prototype.update = function (dt) {
278
  if (!this.freeCam || !this.kb) return;
279
 
280
- // Déplacements : flèches OU ZQSD (AZERTY)
281
  var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
282
  (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
283
 
@@ -286,27 +614,26 @@ FreeCameraInputKeyboard.prototype.update = function (dt) {
286
 
287
  if (fwd !== 0 || strf !== 0) {
288
  var cam = this.entity;
289
- var pos = cam.getPosition().clone();
290
 
291
- var forward = cam.forward.clone(); if (forward.lengthSq() > 1e-8) forward.normalize();
292
- var right = cam.right.clone(); if (right.lengthSq() > 1e-8) right.normalize();
293
 
294
- pos.add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt));
295
- pos.add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
 
296
 
297
- this.freeCam._clampPosition(pos);
298
- cam.setPosition(pos);
 
299
  }
300
 
301
- // Rotation au clavier (Shift + flèches) — conservée
302
  var shift = this.kb.isPressed(pc.KEY_SHIFT);
303
  if (shift) {
304
- var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0); // ← CCW / → CW
305
- var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0); // ↑ up / ↓ down
306
-
307
- // Même logique que la souris : rotation sur place
308
- var yawSpeed = 120; // deg/s
309
- var pitchSpeed = 90; // deg/s
310
  if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
311
  if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
312
  }
 
1
  // ctrl_camera_pr_env.js
2
  // ============================================================================
3
+ // FREE CAMERA (sans orbite) + COLLISION "FIRST-PERSON" style PlayCanvas
4
+ // - Souris : rotation libre (FPS)
5
  // - ZQSD / Flèches : déplacement local (avant/arrière & strafe)
6
+ // - Molette / Pinch : dolly (avance/recul le long du regard)
7
+ // - Collisions : Capsule (caméra) vs AABBs dérivés des meshes sous focusEntity
8
+ // - Step offset (monte les marches) + Snap down (redescend sur le sol)
9
+ // - Pas d'auto-spawn : la position JSON est respectée strictement
10
  // ============================================================================
11
 
12
+ var FreeCamera = pc.createScript('orbitCamera'); // garder le nom public pour compat viewer
13
 
14
  // ======================== Attributs ===========================
15
+ // Look
16
+ FreeCamera.attributes.add('inertiaFactor', { type: 'number', default: 0.10, title: 'Inertia (rotation)' });
 
17
  FreeCamera.attributes.add('pitchAngleMin', { type: 'number', default: -89, title: 'Pitch Min (deg)' });
18
  FreeCamera.attributes.add('pitchAngleMax', { type: 'number', default: 89, title: 'Pitch Max (deg)' });
19
 
20
+ // Sol mini (filet de sécurité)
21
+ FreeCamera.attributes.add('minY', { type: 'number', default: -10, title: 'Minimum camera Y' });
22
+
23
+ // Vitesse (m/s)
24
+ FreeCamera.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
25
+ FreeCamera.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
26
+ FreeCamera.attributes.add('dollySpeed', { type: 'number', default: 2.2, title: 'Mouse/Pinch Dolly Speed' });
27
+
28
+ // Capsule caméra
29
+ FreeCamera.attributes.add('capsuleRadius', { type: 'number', default: 0.30, title: 'Capsule Radius (m)' });
30
+ FreeCamera.attributes.add('capsuleHeight', { type: 'number', default: 1.60, title: 'Capsule Height (m) — yeux à ~0.9m au-dessus du centre' });
31
+ FreeCamera.attributes.add('collisionEps', { 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
+ // Escaliers
38
+ FreeCamera.attributes.add('stepHeight', { type: 'number', default: 0.35, title: 'Max step-up height (m)' });
39
+ FreeCamera.attributes.add('stepAhead', { type: 'number', default: 0.20, title: 'Probe distance ahead for step (m)' });
40
+ FreeCamera.attributes.add('snapDownMax', { type: 'number', default: 0.60, title: 'Max snap-down (m)' });
41
+ FreeCamera.attributes.add('enableGroundSnap', { type: 'boolean', default: true, title: 'Enable ground snap' });
42
 
43
+ // AABBs (construction "indoor-safe")
44
+ FreeCamera.attributes.add('inflateBias', { type: 'number', default: 0.0, title: 'Extra inflate AABB (m)' });
45
+ FreeCamera.attributes.add('mergeGap', { type: 'number', default: 0.0, title: 'Merge AABBs gap (0: chevauchement réel seulement)' });
46
+ FreeCamera.attributes.add('globalCullFrac',{ type: 'number', default: 0.08, title: 'Cull near-global AABBs (0.08=8%)' });
47
+
48
+ // BBox globale optionnelle (filet)
49
  FreeCamera.attributes.add('Xmin', { type: 'number', default: -Infinity, title: 'BBox Xmin' });
50
  FreeCamera.attributes.add('Xmax', { type: 'number', default: Infinity, title: 'BBox Xmax' });
51
  FreeCamera.attributes.add('Ymin', { type: 'number', default: -Infinity, title: 'BBox Ymin' });
 
53
  FreeCamera.attributes.add('Zmin', { type: 'number', default: -Infinity, title: 'BBox Zmin' });
54
  FreeCamera.attributes.add('Zmax', { type: 'number', default: Infinity, title: 'BBox Zmax' });
55
 
56
+ // Compat (pour le viewer)
57
+ FreeCamera.attributes.add('focusEntity', { type: 'entity', title: 'Collision Root (ENV GLB)' });
58
  FreeCamera.attributes.add('frameOnStart', { type: 'boolean', default: false, title: 'Compat: Frame on Start (unused)' });
59
  FreeCamera.attributes.add('yawAngleMin', { type: 'number', default: -360, title: 'Compat: Yaw Min (unused)' });
60
  FreeCamera.attributes.add('yawAngleMax', { type: 'number', default: 360, title: 'Compat: Yaw Max (unused)' });
61
+ FreeCamera.attributes.add('distanceMin', { type: 'number', default: 0.1, title: 'Compat: Distance Min (unused)' });
62
+
63
+ // ======================== Helpers ===========================
64
+ function vAdd(a,b){ return new pc.Vec3(a.x+b.x,a.y+b.y,a.z+b.z); }
65
+ function vSub(a,b){ return new pc.Vec3(a.x-b.x,a.y-b.y,a.z-b.z); }
66
+ function vScale(v,s){ return new pc.Vec3(v.x*s,v.y*s,v.z*s); }
67
+ function vLen(v){ return Math.sqrt(v.x*v.x+v.y*v.y+v.z*v.z); }
68
+ function vDot(a,b){ return a.x*b.x+a.y*b.y+a.z*b.z; }
69
+ function vNorm(v){ var l=vLen(v)||1; return new pc.Vec3(v.x/l,v.y/l,v.z/l); }
70
+ function clamp(v,a,b){ return Math.max(a, Math.min(b,v)); }
71
+
72
+ // Capsule util: point bas et point haut (segment) centrés sur p
73
+ function capsuleSegment(p, height, radius) {
74
+ // Capsule vertical : segment longueur (height - 2*radius), clampée >= 0
75
+ var seg = Math.max(0, height - 2*radius);
76
+ var half = seg * 0.5;
77
+ return {
78
+ a: new pc.Vec3(p.x, p.y - half, p.z),
79
+ b: new pc.Vec3(p.x, p.y + half, p.z),
80
+ len: seg
81
+ };
82
+ }
83
+
84
+ // Distance point-segment au cube AABB (renvoie penetration > 0 si intersecte)
85
+ function capsuleVsAabbPenetration(center, height, radius, aabb, outPush) {
86
+ // approx : on projette les extrémités de la capsule et on prend le pire cas
87
+ var seg = capsuleSegment(center, height, radius);
88
+ // Trouver le point du segment le plus proche de l'AABB (on fait binaire en échantillonnant)
89
+ // Optimisation simple: on teste 3 points: a, b et milieu
90
+ var pts = [seg.a, seg.b, new pc.Vec3((seg.a.x+seg.b.x)/2,(seg.a.y+seg.b.y)/2,(seg.a.z+seg.b.z)/2)];
91
+
92
+ var amin = aabb.getMin();
93
+ var amax = aabb.getMax();
94
+ var eps = 1e-9;
95
+
96
+ var bestPen = 0;
97
+ var bestPush = null;
98
+
99
+ for (var i=0;i<pts.length;i++){
100
+ var p = pts[i];
101
+
102
+ var cx = clamp(p.x, amin.x, amax.x);
103
+ var cy = clamp(p.y, amin.y, amax.y);
104
+ var cz = clamp(p.z, amin.z, amax.z);
105
+
106
+ var dx = p.x - cx, dy = p.y - cy, dz = p.z - cz;
107
+ var d2 = dx*dx + dy*dy + dz*dz;
108
+ var d = Math.sqrt(Math.max(d2, eps));
109
+ var pen = radius - d;
110
+
111
+ if (pen > bestPen) {
112
+ if (d > 1e-6) {
113
+ bestPush = new pc.Vec3(dx/d * pen, dy/d * pen, dz/d * pen);
114
+ } else {
115
+ // si coïncident, pousse le long de l'axe le plus proche
116
+ var ex = Math.min(Math.abs(p.x - amin.x), Math.abs(amax.x - p.x));
117
+ var ey = Math.min(Math.abs(p.y - amin.y), Math.abs(amax.y - p.y));
118
+ var ez = Math.min(Math.abs(p.z - amin.z), Math.abs(amax.z - p.z));
119
+ if (ex <= ey && ex <= ez) bestPush = new pc.Vec3((Math.abs(p.x - amin.x) < Math.abs(amax.x - p.x) ? -1:1)*pen,0,0);
120
+ else if (ey <= ex && ey <= ez) bestPush = new pc.Vec3(0,(Math.abs(p.y - amin.y) < Math.abs(amax.y - p.y) ? -1:1)*pen,0);
121
+ else bestPush = new pc.Vec3(0,0,(Math.abs(p.z - amin.z) < Math.abs(amax.z - p.z) ? -1:1)*pen);
122
+ }
123
+ bestPen = pen;
124
+ }
125
+ }
126
+
127
+ if (bestPen > 0 && outPush) outPush.copy(bestPush);
128
+ return bestPen;
129
+ }
130
 
131
+ // ======================== Getters pitch/yaw ===========================
132
  Object.defineProperty(FreeCamera.prototype, 'pitch', {
133
  get: function () { return this._targetPitch; },
134
  set: function (v) { this._targetPitch = pc.math.clamp(v, this.pitchAngleMin, this.pitchAngleMax); }
135
  });
136
  Object.defineProperty(FreeCamera.prototype, 'yaw', {
137
  get: function () { return this._targetYaw; },
138
+ set: function (v) { this._targetYaw = v; }
139
  });
140
 
141
+ // ======================== Init ===========================
142
  FreeCamera.prototype.initialize = function () {
143
+ // angles init
144
  var q = this.entity.getRotation();
145
+ var f = new pc.Vec3(); q.transformVector(pc.Vec3.FORWARD, f);
 
146
 
147
+ this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
 
 
148
  var yawQuat = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
149
  var noYawQ = new pc.Quat().mul2(yawQuat, q);
150
+ var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
151
+ this._pitch = pc.math.clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchAngleMin, this.pitchAngleMax);
 
 
152
 
153
+ this._targetYaw = this._yaw;
154
  this._targetPitch = this._pitch;
 
 
155
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
156
 
157
+ // état partagé
158
  this.app.systems.script.app.freeCamState = this.app.systems.script.app.freeCamState || {};
159
+ this.state = this.app.systems.script.app.freeCamState;
160
+
161
+ // Construire les colliders depuis focusEntity (GLB environnement)
162
+ this._buildAabbsFromFocus();
163
+
164
+ // Clamp très léger au spawn (sans déplacement “loin”)
165
+ var p0 = this.entity.getPosition().clone();
166
+ var p1 = this._resolveCapsuleCollisions(p0, this.maxResolveIters);
167
+ if (vLen(vSub(p1,p0)) > this.capsuleRadius * 0.25) {
168
+ // limite la correction pour ne PAS expulser hors bâtiment
169
+ var dir = vSub(p1,p0); var L = vLen(dir)||1;
170
+ p1 = vAdd(p0, vScale(dir, (this.capsuleRadius*0.25)/L));
171
+ }
172
+ this._clampBBoxMinY(p1);
173
+ this.entity.setPosition(p1);
174
 
175
+ // Aspect ratio
176
  var self = this;
177
  this._onResize = function(){ self._checkAspectRatio(); };
178
  window.addEventListener('resize', this._onResize, false);
 
180
  };
181
 
182
  FreeCamera.prototype.update = function (dt) {
183
+ // rotation lissée
184
  var t = this.inertiaFactor === 0 ? 1 : Math.min(dt / this.inertiaFactor, 1);
185
  this._yaw = pc.math.lerp(this._yaw, this._targetYaw, t);
186
  this._pitch = pc.math.lerp(this._pitch, this._targetPitch, t);
187
  this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
188
 
189
+ // maintien propre (au cas )
190
  var pos = this.entity.getPosition().clone();
191
+ pos = this._resolveCapsuleCollisions(pos, this.maxResolveIters);
192
+ this._clampBBoxMinY(pos);
193
  this.entity.setPosition(pos);
194
  };
195
 
 
199
  this.entity.camera.horizontalFov = (gd.height > gd.width);
200
  };
201
 
202
+ // ======================== Build colliders ===========================
203
+ FreeCamera.prototype._buildAabbsFromFocus = function () {
204
+ this._colliders = [];
205
+ this._worldAabb = null;
206
+ this._useCollision = false;
207
+
208
+ var root = this.focusEntity;
209
+ if (!root) return;
210
+
211
+ // 1) collecter
212
+ var boxes = [];
213
+ var stack = [root];
214
+ while (stack.length) {
215
+ var e = stack.pop();
216
+ var rc = e.render;
217
+ if (rc && rc.meshInstances && rc.meshInstances.length) {
218
+ for (var i=0;i<rc.meshInstances.length;i++){
219
+ var bb = rc.meshInstances[i].aabb;
220
+ boxes.push(new pc.BoundingBox(bb.center.clone(), bb.halfExtents.clone()));
221
+ }
222
+ }
223
+ var ch = e.children;
224
+ if (ch && ch.length) for (var j=0;j<ch.length;j++) stack.push(ch[j]);
225
+ }
226
+ if (boxes.length === 0) return;
227
+
228
+ // 2) monde
229
+ var world = boxes[0].clone();
230
+ for (var k=1;k<boxes.length;k++) world.add(boxes[k]);
231
+
232
+ // 3) filtrer quasi-global
233
+ var frac = pc.math.clamp(this.globalCullFrac, 0, 0.49);
234
+ var wh = world.halfExtents;
235
+ var filtered = [];
236
+ for (var m=0;m<boxes.length;m++){
237
+ var h = boxes[m].halfExtents;
238
+ var nearGlobal = (h.x >= wh.x*(1-frac)) && (h.y >= wh.y*(1-frac)) && (h.z >= wh.z*(1-frac));
239
+ if (!nearGlobal) filtered.push(boxes[m]);
240
+ }
241
+
242
+ // 4) merge strict
243
+ var merged = this._mergeAabbs(filtered, Math.max(0, this.mergeGap||0));
244
+
245
+ // 5) petit gonflage
246
+ var inflate = Math.max(0, this.inflateBias||0);
247
+ for (var n=0;n<merged.length;n++){
248
+ merged[n].halfExtents.add(new pc.Vec3(inflate, inflate, inflate));
249
+ }
250
+
251
+ // 6) enregistrer
252
+ if (merged.length>0) {
253
+ for (var t=0;t<merged.length;t++) this._colliders.push({ aabb: merged[t] });
254
+ this._worldAabb = world;
255
+ this._useCollision = true;
256
+ }
257
+ };
258
+
259
+ FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
260
+ if (!boxes || boxes.length<=1) return boxes.slice();
261
+ var out = boxes.slice();
262
+ var tol = Math.max(0, gap||0);
263
+ var changed = true;
264
+
265
+ function overlap(a,b,t){
266
+ var amin=a.getMin(), amax=a.getMax();
267
+ var bmin=b.getMin(), bmax=b.getMax();
268
+ return !(
269
+ amax.x < bmin.x - t || amin.x > bmax.x + t ||
270
+ amax.y < bmin.y - t || amin.y > bmax.y + t ||
271
+ amax.z < bmin.z - t || amin.z > bmax.z + t
272
+ );
273
+ }
274
+
275
+ while (changed){
276
+ changed=false;
277
+ var next=[], used=new Array(out.length).fill(false);
278
+ for (var i=0;i<out.length;i++){
279
+ if (used[i]) continue;
280
+ var acc = out[i];
281
+ for (var j=i+1;j<out.length;j++){
282
+ if (used[j]) continue;
283
+ if (overlap(acc,out[j],tol)){
284
+ var aMin=acc.getMin(), aMax=acc.getMax();
285
+ var bMin=out[j].getMin(), bMax=out[j].getMax();
286
+ var nMin=new pc.Vec3(Math.min(aMin.x,bMin.x), Math.min(aMin.y,bMin.y), Math.min(aMin.z,bMin.z));
287
+ var nMax=new pc.Vec3(Math.max(aMax.x,bMax.x), Math.max(aMax.y,bMax.y), Math.max(aMax.z,bMax.z));
288
+ var c=nMin.clone().add(nMax).mulScalar(0.5);
289
+ var h=new pc.Vec3(Math.abs(nMax.x-c.x), Math.abs(nMax.y-c.y), Math.abs(nMax.z-c.z));
290
+ acc = new pc.BoundingBox(c,h);
291
+ used[j]=true; changed=true;
292
+ }
293
+ }
294
+ used[i]=true; next.push(acc);
295
+ }
296
+ out=next;
297
+ }
298
+ return out;
299
+ };
300
+
301
+ // ======================== BBox globale + minY ===========================
302
  FreeCamera.prototype._bboxEnabled = function () {
303
  return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
304
  };
305
+ FreeCamera.prototype._clampBBoxMinY = function (p) {
 
 
306
  if (p.y < this.minY) p.y = this.minY;
307
  if (!this._bboxEnabled()) return;
308
+ p.x = clamp(p.x, this.Xmin, this.Xmax);
309
+ p.y = clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
310
+ p.z = clamp(p.z, this.Zmin, this.Zmax);
311
+ };
312
+
313
+ // ======================== Capsule vs AABBs : resolve ===========================
314
+ FreeCamera.prototype._resolveCapsuleCollisions = function (pos, maxIters) {
315
+ if (!this._useCollision) return pos.clone();
316
+
317
+ var p = pos.clone();
318
+ var R = Math.max(0, this.capsuleRadius);
319
+ var H = Math.max(2*R, this.capsuleHeight); // clamp min
320
+ var eps = Math.max(1e-7, this.collisionEps);
321
+
322
+ if (!this._colliders || this._colliders.length===0) return p;
323
+
324
+ var iters = Math.max(1, maxIters||1);
325
+ for (var iter=0; iter<iters; iter++){
326
+ var moved = false;
327
+
328
+ for (var i=0;i<this._colliders.length;i++){
329
+ var aabb = this._colliders[i].aabb;
330
+ var push = new pc.Vec3();
331
+ var pen = capsuleVsAabbPenetration(p, H, R, aabb, push);
332
+ if (pen > eps) {
333
+ p.add(push);
334
+ moved = true;
335
+ }
336
+ }
337
+ if (!moved) break;
338
+ }
339
+ return p;
340
+ };
341
+
342
+ // ======================== Mvt principal : swept + step + snap ===========================
343
+ FreeCamera.prototype._moveSwept = function (from, delta) {
344
+ if (!this._useCollision) return vAdd(from, delta);
345
+
346
+ var maxStep = Math.max(0.01, this.maxStepDistance||0.2);
347
+ var dist = vLen(delta);
348
+ if (dist <= maxStep) return this._moveStep(from, delta);
349
+
350
+ var steps = Math.ceil(dist / maxStep);
351
+ var step = vScale(delta, 1/steps);
352
+ var cur = from.clone();
353
+ for (var i=0;i<steps;i++) cur = this._moveStep(cur, step);
354
+ return cur;
355
+ };
356
+
357
+ FreeCamera.prototype._moveStep = function (from, delta) {
358
+ // A) tentative directe
359
+ var target = vAdd(from, delta);
360
+ var after = this._resolveCapsuleCollisions(target, this.maxResolveIters);
361
+ var collided = (vLen(vSub(after, target)) > 0);
362
+
363
+ if (!collided) {
364
+ if (this.enableGroundSnap) after = this._snapDown(after);
365
+ return after;
366
+ }
367
+
368
+ // B) slide (supprimer la composante contre la "normale" approx)
369
+ var n = this._estimateNormal(after);
370
+ if (n) {
371
+ var desire = delta.clone();
372
+ // projeter le déplacement sur le plan perpendiculaire à n
373
+ var slide = vSub(desire, vScale(n, vDot(desire, n)));
374
+ var slideTarget = vAdd(from, slide);
375
+ var slideAfter = this._resolveCapsuleCollisions(slideTarget, this.maxResolveIters);
376
+
377
+ // C) step-up si slide insuffisant
378
+ if (vLen(vSub(slideAfter, slideTarget)) > 0) {
379
+ var stepped = this._tryStepUp(from, desire);
380
+ if (stepped) {
381
+ if (this.enableGroundSnap) stepped = this._snapDown(stepped);
382
+ return stepped;
383
+ }
384
+ }
385
+
386
+ if (this.enableGroundSnap) slideAfter = this._snapDown(slideAfter);
387
+ return slideAfter;
388
+ }
389
+
390
+ if (this.enableGroundSnap) after = this._snapDown(after);
391
+ return after;
392
+ };
393
 
394
+ // Normale approx via micro-probes
395
+ FreeCamera.prototype._estimateNormal = function (p) {
396
+ if (!this._colliders) return null;
397
+ var probe = 0.02;
398
+ var base = this._resolveCapsuleCollisions(p, this.maxResolveIters);
399
+ var nx = this._resolveCapsuleCollisions(new pc.Vec3(p.x+probe,p.y,p.z), this.maxResolveIters);
400
+ var px = this._resolveCapsuleCollisions(new pc.Vec3(p.x-probe,p.y,p.z), this.maxResolveIters);
401
+ var ny = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y+probe,p.z), this.maxResolveIters);
402
+ var py = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y-probe,p.z), this.maxResolveIters);
403
+ var nz = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y,p.z+probe), this.maxResolveIters);
404
+ var pz = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y,p.z-probe), this.maxResolveIters);
405
+
406
+ function d(a,b){ return vLen(vSub(a,b)); }
407
+ var dx = d(nx,base) - d(px,base);
408
+ var dy = d(ny,base) - d(py,base);
409
+ var dz = d(nz,base) - d(pz,base);
410
+ var n = new pc.Vec3(dx,dy,dz);
411
+ var L = vLen(n);
412
+ if (L < 1e-5) return null;
413
+ return vScale(n, 1/L);
414
  };
415
 
416
+ // Step-up : monter une marche (jusqu'à stepHeight)
417
+ FreeCamera.prototype._tryStepUp = function (from, wishDelta) {
418
+ var R = Math.max(0, this.capsuleRadius);
419
+ var H = Math.max(2*R, this.capsuleHeight);
420
+ var eps = Math.max(1e-4, this.collisionEps);
421
+ var maxH = Math.max(0, this.stepHeight || 0.35);
422
+ var ahead = Math.max(0.05, this.stepAhead || 0.20);
423
+
424
+ var horiz = new pc.Vec3(wishDelta.x, 0, wishDelta.z);
425
+ var hLen = vLen(horiz);
426
+ if (hLen < 1e-6) return null;
427
+ horiz = vScale(horiz, 1/hLen);
428
+
429
+ // on avance un peu, on monte, puis on applique le delta complet
430
+ var probe = vAdd(from, vScale(horiz, Math.min(hLen, ahead)));
431
+ // tester plusieurs hauteurs jusqu'à maxH
432
+ var trials = 3;
433
+ for (var i=1;i<=trials;i++){
434
+ var up = maxH * (i/trials);
435
+ var raised = new pc.Vec3(probe.x, probe.y + up, probe.z);
436
+ raised = this._resolveCapsuleCollisions(raised, this.maxResolveIters);
437
+ var stepped = this._resolveCapsuleCollisions(vAdd(raised, wishDelta), this.maxResolveIters);
438
+
439
+ // validé si on a gagné horizontalement
440
+ if (vLen(vSub(stepped, raised)) > 0.02) return stepped;
441
+ }
442
+ return null;
443
+ };
444
+
445
+ // Snap-down : redéposer sur le sol si on flotte un peu (descente d'escalier)
446
+ FreeCamera.prototype._snapDown = function (p) {
447
+ var R = Math.max(0, this.capsuleRadius);
448
+ var H = Math.max(2*R, this.capsuleHeight);
449
+ var maxDown = Math.max(0, this.snapDownMax || 0.6);
450
+
451
+ // On descend par petits incréments pour “chercher” le sol
452
+ var steps = 4;
453
+ var step = maxDown / steps;
454
+ var cur = p.clone();
455
+ for (var i=0;i<steps;i++){
456
+ var down = new pc.Vec3(cur.x, cur.y - step, cur.z);
457
+ var resolved = this._resolveCapsuleCollisions(down, this.maxResolveIters);
458
+ if (vLen(vSub(resolved, down)) > 0) {
459
+ // on a tapé quelque chose : remonter légèrement et arrêter
460
+ return this._resolveCapsuleCollisions(new pc.Vec3(resolved.x, resolved.y + 0.01, resolved.z), this.maxResolveIters);
461
+ }
462
+ cur.copy(down);
463
+ }
464
+ return p;
465
+ };
466
+
467
+ // ===================== INPUT SOURIS =====================
468
+ var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse');
469
  FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' });
470
  FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
471
 
472
  FreeCameraInputMouse.prototype.initialize = function () {
473
+ this.freeCam = this.entity.script.orbitCamera;
474
  this.last = new pc.Vec2();
475
  this.isLooking = false;
476
 
 
497
  };
498
 
499
  FreeCameraInputMouse.prototype.onMouseDown = function (e) {
 
500
  this.isLooking = true;
501
  this.last.set(e.x, e.y);
502
  };
 
508
  FreeCameraInputMouse.prototype.onMouseMove = function (e) {
509
  if (!this.isLooking || !this.freeCam) return;
510
  var sens = this.lookSensitivity;
511
+ this.freeCam.yaw = this.freeCam.yaw - e.dx * sens;
512
+ this.freeCam.pitch = this.freeCam.pitch - e.dy * sens;
 
 
 
 
 
 
513
  this.last.set(e.x, e.y);
514
  };
515
 
516
  FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
517
  if (!this.freeCam) return;
 
 
518
  var cam = this.entity;
519
+ var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
520
+ var fwd = cam.forward.clone(); if (fwd.lengthSq()>1e-8) fwd.normalize();
521
 
522
+ var from = cam.getPosition().clone();
523
+ var to = from.clone().add(fwd.mulScalar(move));
524
+ var next = this.freeCam._moveSwept(from, to.sub(from));
525
+ this.freeCam._clampBBoxMinY(next);
526
+ cam.setPosition(next);
527
 
528
  e.event.preventDefault();
529
  };
530
 
531
+ // ===================== INPUT TOUCH =====================
532
  var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
533
+ FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
534
+ FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
535
 
536
  FreeCameraInputTouch.prototype.initialize = function () {
537
  this.freeCam = this.entity.script.orbitCamera;
 
562
  this.isLooking = (e.event.type === 'touchstart');
563
  this.last.set(t[0].x, t[0].y);
564
  } else if (t.length === 2) {
565
+ var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
 
566
  this.lastPinch = Math.sqrt(dx*dx + dy*dy);
567
  } else {
568
  this.isLooking = false;
 
574
  if (!this.freeCam) return;
575
 
576
  if (t.length === 1 && this.isLooking) {
577
+ var s = this.lookSensitivity;
578
+ var dx = t[0].x - this.last.x, dy = t[0].y - this.last.y;
579
+ this.freeCam.yaw = this.freeCam.yaw - dx * s;
580
+ this.freeCam.pitch = this.freeCam.pitch - dy * s;
 
 
 
581
  this.last.set(t[0].x, t[0].y);
582
  } else if (t.length === 2) {
583
+ var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
 
 
584
  var dist = Math.sqrt(dx*dx + dy*dy);
585
+ var delta = dist - this.lastPinch; this.lastPinch = dist;
 
586
 
587
  var cam = this.entity;
588
+ var fwd = cam.forward.clone(); if (fwd.lengthSq()>1e-8) fwd.normalize();
589
+ var from = cam.getPosition().clone();
590
+ var to = from.clone().add(fwd.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
591
+ var next = this.freeCam._moveSwept(from, to.sub(from));
592
+ this.freeCam._clampBBoxMinY(next);
593
+ cam.setPosition(next);
594
  }
595
  };
596
 
597
+ // ===================== INPUT CLAVIER =====================
598
  var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
 
599
  FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused, future)' });
600
 
601
  FreeCameraInputKeyboard.prototype.initialize = function () {
 
606
  FreeCameraInputKeyboard.prototype.update = function (dt) {
607
  if (!this.freeCam || !this.kb) return;
608
 
 
609
  var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
610
  (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
611
 
 
614
 
615
  if (fwd !== 0 || strf !== 0) {
616
  var cam = this.entity;
617
+ var from = cam.getPosition().clone();
618
 
619
+ var forward = cam.forward.clone(); if (forward.lengthSq()>1e-8) forward.normalize();
620
+ var right = cam.right.clone(); if (right.lengthSq()>1e-8) right.normalize();
621
 
622
+ var delta = new pc.Vec3()
623
+ .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
624
+ .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
625
 
626
+ var next = this.freeCam._moveSwept(from, delta);
627
+ this.freeCam._clampBBoxMinY(next);
628
+ cam.setPosition(next);
629
  }
630
 
631
+ // Rotation clavier (Shift + flèches)
632
  var shift = this.kb.isPressed(pc.KEY_SHIFT);
633
  if (shift) {
634
+ var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0);
635
+ var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0);
636
+ var yawSpeed = 120, pitchSpeed = 90;
 
 
 
637
  if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
638
  if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
639
  }