MikaFil commited on
Commit
97e1c09
·
verified ·
1 Parent(s): f371f1f

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,404 +1,642 @@
1
- // ctrl_camera_rigidbody_fps.js
2
  // ============================================================================
3
- // Contrôle caméra pour la version "physique" (Ammo) avec player capsule dynamique.
4
- // - À attacher sur l'entité **Player** (rigidbody capsule) créée par viewer_pr_env.js
5
- // - La caméra doit être **enfant** de Player (hauteur des yeux). Le script gère:
6
- // * Souris / Touch : yaw (Player) + pitch (Camera enfant)
7
- // * Clavier : ZQSD/WSAD/Flèches + E/Space (monter) + A/Ctrl (descendre) + Shift (dash)
8
- // * Molette / Pinch : avancer/reculer (dolly) en ajustant la vélocité
9
- // * Modes : Free-fly (gravité nulle) ou Grounded (gravité, jump)
10
- // - Fonctionne aussi si les attributs ne sont pas renseignés: auto-détection Player/Camera
11
  // ============================================================================
12
 
13
- /*
14
- UTILISATION
15
- ----------
16
- 1) Ajoute ce fichier en ScriptAsset sur l'app.
17
- 2) Sur l'entité Player (capsule), ajoute un ScriptComponent et crée "fpController".
18
- - cameraChild: (optionnel) référence vers ta caméra enfant. Si vide, pris 1er child avec component "camera".
19
- 3) (Optionnel) Ajoute aussi fpInputMouse, fpInputTouch, fpInputKeyboard sur la même entité (Player).
20
- Sinon fpController attache des listeners par défaut (mais séparer reste plus propre).
21
 
22
- NB: Si tu utilises le viewer fourni, la caméra est déjà enfant de Player.
23
- */
24
-
25
- var FPC = pc.createScript('fpController');
 
26
 
27
- // ----------------------- Attributs -----------------------
28
- FPC.attributes.add('cameraChild', { type: 'entity', title: 'Camera (child of Player)' });
29
- FPC.attributes.add('freeFly', { type: 'boolean', default: true, title: 'Free Fly (no gravity)' });
30
 
31
  // Vitesse (m/s)
32
- FPC.attributes.add('moveSpeed', { type: 'number', default: 2.2, title: 'Move Speed' });
33
- FPC.attributes.add('strafeSpeed', { type: 'number', default: 2.2, title: 'Strafe Speed' });
34
- FPC.attributes.add('verticalSpeed', { type: 'number', default: 2.0, title: 'Vertical Speed (free-fly)'});
35
- FPC.attributes.add('dashMultiplier', { type: 'number', default: 1.8, title: 'Shift Dash Multiplier' });
36
- FPC.attributes.add('maxVelocity', { type: 'number', default: 6.5, title: 'Max Linear Velocity (clamp)' });
37
-
38
- // Saut (grounded)
39
- FPC.attributes.add('jumpImpulse', { type: 'number', default: 4.6, title: 'Jump Impulse (grounded)' });
40
- FPC.attributes.add('groundCheckOffset', { type: 'number', default: 0.05, title: 'Ground Check Extra (m)' });
41
- FPC.attributes.add('groundMaxSlopeDeg', { type: 'number', default: 55, title: 'Ground Max Slope (deg)' });
42
-
43
- // Look
44
- FPC.attributes.add('lookSensitivity', { type: 'number', default: 0.27, title: 'Mouse/Touch Look Sensitivity' });
45
- FPC.attributes.add('invertY', { type: 'boolean', default: false, title: 'Invert Y' });
46
- FPC.attributes.add('pitchMin', { type: 'number', default: -85, title: 'Pitch Min (deg)' });
47
- FPC.attributes.add('pitchMax', { type: 'number', default: 85, title: 'Pitch Max (deg)' });
48
- FPC.attributes.add('smoothing', { type: 'number', default: 0.18, title: 'Velocity Lerp (0..1s)'});
49
-
50
- // Dolly / Pinch
51
- FPC.attributes.add('wheelFactor', { type: 'number', default: 0.9, title: 'Wheel Factor (m per notch)'});
52
- FPC.attributes.add('pinchFactor', { type: 'number', default: 0.02, title: 'Pinch Factor'});
53
-
54
- // Debug
55
- FPC.attributes.add('debugRays', { type: 'boolean', default: false, title: 'Debug Ground Raycasts (console)'});
56
-
57
- // ----------------------- Helpers -----------------------
58
- function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); }
59
- function toRad(deg){ return deg * pc.math.DEG_TO_RAD; }
60
- function horiz(v){ return new pc.Vec3(v.x, 0, v.z); }
61
-
62
- // ----------------------- Lifecycle -----------------------
63
- FPC.prototype.initialize = function(){
64
- this.app.mouse && this.app.mouse.disableContextMenu();
65
-
66
- // Trouver la caméra enfant si non liée
67
- if (!this.cameraChild) {
68
- var found = null;
69
- for (var i=0;i<this.entity.children.length;i++){
70
- var c = this.entity.children[i];
71
- if (c.camera) { found = c; break; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
- this.cameraChild = found || null;
74
  }
75
 
76
- // État yaw/pitch depuis rotations actuelles
77
- var cam = this.cameraChild || this.entity; // fallback
78
- var qCam = cam.getRotation();
79
- var f = new pc.Vec3(); qCam.transformVector(pc.Vec3.FORWARD, f);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- // yaw sur Player, pitch sur Camera
82
  this._yaw = Math.atan2(f.x, f.z) * pc.math.RAD_TO_DEG;
83
  var yawQuat = new pc.Quat().setFromEulerAngles(0, -this._yaw, 0);
84
- var noYawQ = new pc.Quat().mul2(yawQuat, qCam);
85
  var fNoYaw = new pc.Vec3(); noYawQ.transformVector(pc.Vec3.FORWARD, fNoYaw);
86
- this._pitch = clamp(Math.atan2(-fNoYaw.y, -fNoYaw.z) * pc.math.RAD_TO_DEG, this.pitchMin, this.pitchMax);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- // Inputs accumulés
89
- this._wish = new pc.Vec3(0,0,0); // input axes
90
- this._wishLook = new pc.Vec2(0,0); // dx, dy
 
 
 
91
 
92
- // Touch state
93
- this._touchLookActive = false;
94
- this._lastTouchDist = 0;
 
 
 
 
 
 
 
 
 
 
95
 
96
- // Grounded cache
97
- this._isGrounded = false;
 
 
 
98
 
99
- // Pointer lock (optionnel) — simple clic gauche
100
- var self = this;
101
- if (this.app.mouse) {
102
- this.app.mouse.on(pc.EVENT_MOUSEDOWN, function(e){
103
- if (e.button === pc.MOUSEBUTTON_LEFT && document.activeElement === self.app.graphicsDevice.canvas) {
104
- if (self.app.graphicsDevice.canvas.requestPointerLock) {
105
- self.app.graphicsDevice.canvas.requestPointerLock();
106
- }
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
- }, this);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
- // Listeners de base si pas d'inputs dédiés
112
- this._attachDefaultListeners();
113
-
114
- // Clamp pitch & appliquer orientation initiale
115
- this._applyOrientationImmediate();
116
- };
117
 
118
- FPC.prototype._attachDefaultListeners = function(){
119
- var self = this;
120
- if (this.app.mouse){
121
- this._onMouseMove = function(e){ if (document.pointerLockElement !== self.app.graphicsDevice.canvas) return; self._wishLook.x += e.dx; self._wishLook.y += e.dy; };
122
- this._onWheel = function(e){ self._dollyByWheel(e.wheelDelta); e.event.preventDefault(); };
123
- this.app.mouse.on(pc.EVENT_MOUSEMOVE, this._onMouseMove, this);
124
- this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this._onWheel, this);
125
- }
126
- if (this.app.touch){
127
- this._onTStart = this._touchStart.bind(this);
128
- this._onTMove = this._touchMove.bind(this);
129
- this._onTEnd = this._touchEnd.bind(this);
130
- this.app.touch.on(pc.EVENT_TOUCHSTART, this._onTStart, this);
131
- this.app.touch.on(pc.EVENT_TOUCHMOVE, this._onTMove, this);
132
- this.app.touch.on(pc.EVENT_TOUCHEND, this._onTEnd, this);
133
- this.app.touch.on(pc.EVENT_TOUCHCANCEL,this._onTEnd, this);
134
  }
135
 
136
- if (this.app.keyboard){
137
- this._kb = this.app.keyboard;
 
 
 
138
  }
 
 
139
  };
140
 
141
- FPC.prototype._detachDefaultListeners = function(){
142
- if (this.app.mouse && this._onMouseMove){
143
- this.app.mouse.off(pc.EVENT_MOUSEMOVE, this._onMouseMove, this);
144
- this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this._onWheel, this);
 
 
 
 
 
 
 
 
 
 
145
  }
146
- if (this.app.touch && this._onTStart){
147
- this.app.touch.off(pc.EVENT_TOUCHSTART, this._onTStart, this);
148
- this.app.touch.off(pc.EVENT_TOUCHMOVE, this._onTMove, this);
149
- this.app.touch.off(pc.EVENT_TOUCHEND, this._onTEnd, this);
150
- this.app.touch.off(pc.EVENT_TOUCHCANCEL,this._onTEnd, this);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
 
152
  };
153
 
154
- FPC.prototype._applyOrientationImmediate = function(){
155
- // Player: yaw
156
- this.entity.setLocalEulerAngles(0, this._yaw, 0);
157
- // Camera (pitch)
158
- var cam = this.cameraChild || this.entity;
159
- cam.setLocalEulerAngles(this._pitch, 0, 0);
160
  };
 
 
 
 
 
 
 
 
 
 
 
161
 
162
- // ----------------------- Update -----------------------
163
- FPC.prototype.update = function(dt){
164
- // 1) Lire inputs clavier et former le vecteur d'intention
165
- var kb = this._kb;
166
- var fwdAxis = 0, strAxis = 0, upAxis = 0, dash = 0;
167
- if (kb) {
168
- // Avant/arrière — support ZQSD/WSAD/Flèches
169
- fwdAxis = ((kb.isPressed(pc.KEY_UP) || kb.isPressed(pc.KEY_W) || kb.isPressed(pc.KEY_Z)) ? 1 : 0)
170
- + ((kb.isPressed(pc.KEY_DOWN) || kb.isPressed(pc.KEY_S)) ? -1 : 0);
171
- // Strafe
172
- strAxis = ((kb.isPressed(pc.KEY_RIGHT) || kb.isPressed(pc.KEY_D)) ? 1 : 0)
173
- + ((kb.isPressed(pc.KEY_LEFT) || kb.isPressed(pc.KEY_AZERTY ? pc.KEY_Q : pc.KEY_A)) ? -1 : 0);
174
- // Monter/descendre
175
- upAxis = ((kb.isPressed(pc.KEY_E) || kb.isPressed(pc.KEY_SPACE)) ? 1 : 0)
176
- + ((kb.isPressed(pc.KEY_C) || kb.isPressed(pc.KEY_X) || kb.isPressed(pc.KEY_CTRL)) ? -1 : 0);
177
- dash = kb.isPressed(pc.KEY_SHIFT) ? 1 : 0;
178
-
179
- // Jump (impulsion unique) si grounded & mode grounded
180
- if (!this.freeFly && (kb.wasPressed(pc.KEY_SPACE) || kb.wasPressed(pc.KEY_E))) {
181
- if (this._isGrounded && this.entity.rigidbody) {
182
- var vel = this.entity.rigidbody.linearVelocity.clone();
183
- vel.y = 0; // reset ascendante
184
- this.entity.rigidbody.linearVelocity = vel;
185
- this.entity.rigidbody.applyImpulse(0, this.jumpImpulse, 0);
186
  }
187
  }
 
188
  }
 
 
189
 
190
- // 2) Yaw/pitch depuis souris/touch accumulé
191
- if (this._wishLook.x !== 0 || this._wishLook.y !== 0){
192
- var sens = this.lookSensitivity;
193
- var dx = this._wishLook.x * sens;
194
- var dy = this._wishLook.y * (this.invertY ? 1 : -1) * sens;
195
- this._wishLook.set(0,0);
196
-
197
- this._yaw += dx;
198
- this._pitch = clamp(this._pitch + dy, this.pitchMin, this.pitchMax);
199
- this.entity.setLocalEulerAngles(0, this._yaw, 0);
200
- (this.cameraChild || this.entity).setLocalEulerAngles(this._pitch, 0, 0);
201
- }
202
 
203
- // 3) Mouvement (physique): calcule la vélocité cible
204
- var body = this.entity.rigidbody;
205
- if (!body) return;
206
 
207
- // Base directions (horizontales) dans l'espace Player
208
- var forward = this.entity.forward.clone(); forward.y = 0; if (forward.lengthSq()>1e-8) forward.normalize();
209
- var right = this.entity.right.clone(); right.y = 0; if (right.lengthSq()>1e-8) right.normalize();
 
 
 
210
 
211
- var speedMul = 1 + dash * (this.dashMultiplier - 1);
212
- var wish = new pc.Vec3(0,0,0);
213
- wish.add(forward.mulScalar(fwdAxis * this.moveSpeed * speedMul));
214
- wish.add(right .mulScalar(strAxis * this.strafeSpeed * speedMul));
 
215
 
216
- // Free-fly: composante verticale contrôlée
217
- var curVel = body.linearVelocity.clone();
218
- if (this.freeFly) {
219
- wish.y = upAxis * this.verticalSpeed * speedMul;
220
- } else {
221
- // Grounded: conserve Y (gravité). On autorise une légère descente via C si tu veux, mais en général 0.
222
- wish.y = curVel.y;
223
  }
224
 
225
- // 4) Ground check (pour info + jump)
226
- this._isGrounded = this._checkGrounded();
227
-
228
- // 5) Lerp la vélocité pour un feeling doux
229
- var lerpT = 1 - Math.exp(-dt / Math.max(0.001, this.smoothing));
230
- var target = wish;
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- // Clamp vitesse max (sécurité)
233
- var maxV = Math.max(0.1, this.maxVelocity);
234
- if (this.freeFly) {
235
- if (target.length() > maxV) target.normalize().mulScalar(maxV);
236
- } else {
237
- // clamp horizontal uniquement
238
- var hv = new pc.Vec3(target.x, 0, target.z);
239
- if (hv.length() > maxV) hv.normalize().mulScalar(maxV);
240
- target = new pc.Vec3(hv.x, target.y, hv.z);
241
  }
242
 
243
- var out = new pc.Vec3(
244
- pc.math.lerp(curVel.x, target.x, lerpT),
245
- pc.math.lerp(curVel.y, target.y, lerpT),
246
- pc.math.lerp(curVel.z, target.z, lerpT)
247
- );
248
-
249
- body.linearVelocity = out;
250
  };
251
 
252
- // ----------------------- Ground detection -----------------------
253
- FPC.prototype._checkGrounded = function(){
254
- if (this.freeFly || !this.entity || !this.entity.rigidbody) return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
- // Capsule dims: essaye de récupérer depuis Collision component
257
- var col = this.entity.collision;
258
- var radius = 0.3, height = 1.6;
259
- if (col && col.type === 'capsule') { radius = col.radius; height = col.height; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
- var origin = this.entity.getPosition().clone();
262
- var start = origin.clone();
263
- var extra = Math.max(0.01, this.groundCheckOffset);
264
- var end = origin.clone();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
- // Rayon part un peu au dessous du bas de la capsule vers le bas
267
- var bottom = origin.y - Math.max(radius, (height*0.5 - radius));
268
- start.y = bottom + 0.05;
269
- end.y = bottom - (0.25 + extra);
270
 
271
- var result = this.app.systems.rigidbody.raycastFirst(start, end);
 
 
 
272
 
273
- if (this.debugRays) {
274
- console.log('[FPC] ray:', start, '->', end, 'hit=', !!result);
 
 
 
 
275
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
- if (!result) return false;
 
 
 
 
 
 
 
278
 
279
- // Vérifier pente (angle avec la normale)
280
- var n = result.normal || new pc.Vec3(0,1,0);
281
- var slope = Math.acos(clamp(n.y, -1, 1)) * pc.math.RAD_TO_DEG; // approx: normal.y = cos(angle)
282
- return slope <= this.groundMaxSlopeDeg;
 
 
283
  };
284
 
285
- // ----------------------- Dolly (wheel/pinch) -----------------------
286
- FPC.prototype._dollyByWheel = function(delta){
287
- var body = this.entity.rigidbody; if (!body) return;
288
- var fwd = this.entity.forward.clone();
289
- if (fwd.lengthSq()>1e-8) fwd.normalize();
290
- var amt = -delta * this.wheelFactor * 0.02; // scale doux
291
-
292
- var cur = body.linearVelocity.clone();
293
- var add = fwd.mulScalar(amt);
294
- cur.add(add);
295
- body.linearVelocity = cur;
 
 
296
  };
297
 
298
- FPC.prototype._dollyByPinchDelta = function(d){
299
- var body = this.entity.rigidbody; if (!body) return;
300
- var fwd = this.entity.forward.clone();
301
- if (fwd.lengthSq()>1e-8) fwd.normalize();
302
- var amt = d * this.pinchFactor * this.moveSpeed;
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
- var cur = body.linearVelocity.clone();
305
- var add = fwd.mulScalar(amt);
306
- body.linearVelocity = cur.add(add);
 
 
 
 
 
307
  };
308
 
309
- // ----------------------- Touch handlers -----------------------
310
- FPC.prototype._touchStart = function(e){
311
  var t = e.touches;
312
  if (t.length === 1) {
313
- this._touchLookActive = true;
314
- this._lastTouch = new pc.Vec2(t[0].x, t[0].y);
315
  } else if (t.length === 2) {
316
  var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
317
- this._lastTouchDist = Math.sqrt(dx*dx + dy*dy);
318
- this._touchLookActive = false;
319
  } else {
320
- this._touchLookActive = false;
321
  }
322
  };
323
 
324
- FPC.prototype._touchMove = function(e){
325
  var t = e.touches;
326
- if (t.length === 1 && this._touchLookActive) {
327
- var dx = t[0].x - this._lastTouch.x;
328
- var dy = t[0].y - this._lastTouch.y;
329
- this._wishLook.x += dx;
330
- this._wishLook.y += dy;
331
- this._lastTouch.set(t[0].x, t[0].y);
 
 
332
  } else if (t.length === 2) {
333
  var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
334
  var dist = Math.sqrt(dx*dx + dy*dy);
335
- var delta = dist - this._lastTouchDist; this._lastTouchDist = dist;
336
- this._dollyByPinchDelta(delta);
 
 
 
 
 
 
 
337
  }
338
  };
339
 
340
- FPC.prototype._touchEnd = function(){
341
- this._touchLookActive = false;
342
- };
343
 
344
- FPC.prototype.onDestroy = function(){
345
- this._detachDefaultListeners();
 
346
  };
347
 
348
- // ----------------------- Inputs dédiés (optionnels) -----------------------
349
- // Si tu préfères séparer, tu peux ajouter ces scripts sur Player. Ils pilotent fpController.
350
 
351
- var FPMouse = pc.createScript('fpInputMouse');
352
- FPMouse.attributes.add('sensitivity', { type: 'number', default: 0.27 });
353
- FPMouse.prototype.initialize = function(){
354
- this.ctrl = this.entity.script && this.entity.script.fpController;
355
- if (!this.ctrl || !this.app.mouse) return;
356
- this.app.mouse.disableContextMenu();
357
- var self = this;
358
- this._onMove = function(e){ if (document.pointerLockElement !== self.app.graphicsDevice.canvas) return; self.ctrl._wishLook.x += e.dx * (self.sensitivity / self.ctrl.lookSensitivity); self.ctrl._wishLook.y += e.dy * (self.sensitivity / self.ctrl.lookSensitivity); };
359
- this._onWheel = function(e){ self.ctrl._dollyByWheel(e.wheelDelta); e.event.preventDefault(); };
360
- this.app.mouse.on(pc.EVENT_MOUSEMOVE, this._onMove, this);
361
- this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this._onWheel, this);
362
- this.on('destroy', ()=>{
363
- this.app.mouse.off(pc.EVENT_MOUSEMOVE, this._onMove, this);
364
- this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this._onWheel, this);
365
- });
366
- };
367
 
368
- var FPTouch = pc.createScript('fpInputTouch');
369
- FPTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5 });
370
- FPTouch.attributes.add('pinchFactor', { type: 'number', default: 0.02 });
371
- FPTouch.prototype.initialize = function(){
372
- this.ctrl = this.entity.script && this.entity.script.fpController;
373
- if (!this.ctrl || !this.app.touch) return;
374
- this._onTStart = (e)=>{ this.ctrl._touchStart(e); };
375
- this._onTMove = (e)=>{ this.ctrl._touchMove(e); };
376
- this._onTEnd = (e)=>{ this.ctrl._touchEnd(e); };
377
- this.app.touch.on(pc.EVENT_TOUCHSTART, this._onTStart, this);
378
- this.app.touch.on(pc.EVENT_TOUCHMOVE, this._onTMove, this);
379
- this.app.touch.on(pc.EVENT_TOUCHEND, this._onTEnd, this);
380
- this.app.touch.on(pc.EVENT_TOUCHCANCEL,this._onTEnd, this);
381
- this.on('destroy', ()=>{
382
- this.app.touch.off(pc.EVENT_TOUCHSTART, this._onTStart, this);
383
- this.app.touch.off(pc.EVENT_TOUCHMOVE, this._onTMove, this);
384
- this.app.touch.off(pc.EVENT_TOUCHEND, this._onTEnd, this);
385
- this.app.touch.off(pc.EVENT_TOUCHCANCEL,this._onTEnd, this);
386
- });
387
- };
388
 
389
- var FPKeyboard = pc.createScript('fpInputKeyboard');
390
- FPKeyboard.prototype.initialize = function(){
391
- this.ctrl = this.entity.script && this.entity.script.fpController;
392
- this.kb = this.app.keyboard || null;
393
- };
394
- FPKeyboard.prototype.update = function(){ /* lues directement par fpController */ };
395
 
396
- // ----------------------- API publique -----------------------
397
- FPC.prototype.setFreeFly = function(enabled){
398
- this.freeFly = !!enabled;
399
- if (this.freeFly) {
400
- // Conseil: mets aussi app.scene.gravity = (0,0,0) côté viewer si besoin
 
 
401
  }
402
- };
403
 
404
- FPC.prototype.getFreeFly = function(){ return !!this.freeFly; };
 
 
 
 
 
 
 
 
 
 
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' });
52
+ FreeCamera.attributes.add('Ymax', { type: 'number', default: Infinity, title: 'BBox Ymax' });
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);
179
+ this._checkAspectRatio();
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 où)
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
 
196
+ FreeCamera.prototype._checkAspectRatio = function () {
197
+ var gd = this.app.graphicsDevice;
198
+ if (!gd) return;
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
+ console.log('[FREE-CAM] colliders:', boxes.length);
259
  };
260
 
261
+ FreeCamera.prototype._mergeAabbs = function (boxes, gap) {
262
+ if (!boxes || boxes.length<=1) return boxes.slice();
263
+ var out = boxes.slice();
264
+ var tol = Math.max(0, gap||0);
265
+ var changed = true;
266
+
267
+ function overlap(a,b,t){
268
+ var amin=a.getMin(), amax=a.getMax();
269
+ var bmin=b.getMin(), bmax=b.getMax();
270
+ return !(
271
+ amax.x < bmin.x - t || amin.x > bmax.x + t ||
272
+ amax.y < bmin.y - t || amin.y > bmax.y + t ||
273
+ amax.z < bmin.z - t || amin.z > bmax.z + t
274
+ );
275
  }
276
+
277
+ while (changed){
278
+ changed=false;
279
+ var next=[], used=new Array(out.length).fill(false);
280
+ for (var i=0;i<out.length;i++){
281
+ if (used[i]) continue;
282
+ var acc = out[i];
283
+ for (var j=i+1;j<out.length;j++){
284
+ if (used[j]) continue;
285
+ if (overlap(acc,out[j],tol)){
286
+ var aMin=acc.getMin(), aMax=acc.getMax();
287
+ var bMin=out[j].getMin(), bMax=out[j].getMax();
288
+ var nMin=new pc.Vec3(Math.min(aMin.x,bMin.x), Math.min(aMin.y,bMin.y), Math.min(aMin.z,bMin.z));
289
+ var nMax=new pc.Vec3(Math.max(aMax.x,bMax.x), Math.max(aMax.y,bMax.y), Math.max(aMax.z,bMax.z));
290
+ var c=nMin.clone().add(nMax).mulScalar(0.5);
291
+ var h=new pc.Vec3(Math.abs(nMax.x-c.x), Math.abs(nMax.y-c.y), Math.abs(nMax.z-c.z));
292
+ acc = new pc.BoundingBox(c,h);
293
+ used[j]=true; changed=true;
294
+ }
295
+ }
296
+ used[i]=true; next.push(acc);
297
+ }
298
+ out=next;
299
  }
300
+ return out;
301
  };
302
 
303
+ // ======================== BBox globale + minY ===========================
304
+ FreeCamera.prototype._bboxEnabled = function () {
305
+ return (this.Xmin < this.Xmax) && (this.Ymin < this.Ymax) && (this.Zmin < this.Zmax);
 
 
 
306
  };
307
+ FreeCamera.prototype._clampBBoxMinY = function (p) {
308
+ if (p.y < this.minY) p.y = this.minY;
309
+ if (!this._bboxEnabled()) return;
310
+ p.x = clamp(p.x, this.Xmin, this.Xmax);
311
+ p.y = clamp(p.y, Math.max(this.Ymin, this.minY), this.Ymax);
312
+ p.z = clamp(p.z, this.Zmin, this.Zmax);
313
+ };
314
+
315
+ // ======================== Capsule vs AABBs : resolve ===========================
316
+ FreeCamera.prototype._resolveCapsuleCollisions = function (pos, maxIters) {
317
+ if (!this._useCollision) return pos.clone();
318
 
319
+ var p = pos.clone();
320
+ var R = Math.max(0, this.capsuleRadius);
321
+ var H = Math.max(2*R, this.capsuleHeight); // clamp min
322
+ var eps = Math.max(1e-7, this.collisionEps);
323
+
324
+ if (!this._colliders || this._colliders.length===0) return p;
325
+
326
+ var iters = Math.max(1, maxIters||1);
327
+ for (var iter=0; iter<iters; iter++){
328
+ var moved = false;
329
+
330
+ for (var i=0;i<this._colliders.length;i++){
331
+ var aabb = this._colliders[i].aabb;
332
+ var push = new pc.Vec3();
333
+ var pen = capsuleVsAabbPenetration(p, H, R, aabb, push);
334
+ if (pen > eps) {
335
+ p.add(push);
336
+ moved = true;
 
 
 
 
 
 
337
  }
338
  }
339
+ if (!moved) break;
340
  }
341
+ return p;
342
+ };
343
 
344
+ // ======================== Mvt principal : swept + step + snap ===========================
345
+ FreeCamera.prototype._moveSwept = function (from, delta) {
346
+ if (!this._useCollision) return vAdd(from, delta);
 
 
 
 
 
 
 
 
 
347
 
348
+ var maxStep = Math.max(0.01, this.maxStepDistance||0.2);
349
+ var dist = vLen(delta);
350
+ if (dist <= maxStep) return this._moveStep(from, delta);
351
 
352
+ var steps = Math.ceil(dist / maxStep);
353
+ var step = vScale(delta, 1/steps);
354
+ var cur = from.clone();
355
+ for (var i=0;i<steps;i++) cur = this._moveStep(cur, step);
356
+ return cur;
357
+ };
358
 
359
+ FreeCamera.prototype._moveStep = function (from, delta) {
360
+ // A) tentative directe
361
+ var target = vAdd(from, delta);
362
+ var after = this._resolveCapsuleCollisions(target, this.maxResolveIters);
363
+ var collided = (vLen(vSub(after, target)) > 0);
364
 
365
+ if (!collided) {
366
+ if (this.enableGroundSnap) after = this._snapDown(after);
367
+ return after;
 
 
 
 
368
  }
369
 
370
+ // B) slide (supprimer la composante contre la "normale" approx)
371
+ var n = this._estimateNormal(after);
372
+ if (n) {
373
+ var desire = delta.clone();
374
+ // projeter le déplacement sur le plan perpendiculaire à n
375
+ var slide = vSub(desire, vScale(n, vDot(desire, n)));
376
+ var slideTarget = vAdd(from, slide);
377
+ var slideAfter = this._resolveCapsuleCollisions(slideTarget, this.maxResolveIters);
378
+
379
+ // C) step-up si slide insuffisant
380
+ if (vLen(vSub(slideAfter, slideTarget)) > 0) {
381
+ var stepped = this._tryStepUp(from, desire);
382
+ if (stepped) {
383
+ if (this.enableGroundSnap) stepped = this._snapDown(stepped);
384
+ return stepped;
385
+ }
386
+ }
387
 
388
+ if (this.enableGroundSnap) slideAfter = this._snapDown(slideAfter);
389
+ return slideAfter;
 
 
 
 
 
 
 
390
  }
391
 
392
+ if (this.enableGroundSnap) after = this._snapDown(after);
393
+ return after;
 
 
 
 
 
394
  };
395
 
396
+ // Normale approx via micro-probes
397
+ FreeCamera.prototype._estimateNormal = function (p) {
398
+ if (!this._colliders) return null;
399
+ var probe = 0.02;
400
+ var base = this._resolveCapsuleCollisions(p, this.maxResolveIters);
401
+ var nx = this._resolveCapsuleCollisions(new pc.Vec3(p.x+probe,p.y,p.z), this.maxResolveIters);
402
+ var px = this._resolveCapsuleCollisions(new pc.Vec3(p.x-probe,p.y,p.z), this.maxResolveIters);
403
+ var ny = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y+probe,p.z), this.maxResolveIters);
404
+ var py = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y-probe,p.z), this.maxResolveIters);
405
+ var nz = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y,p.z+probe), this.maxResolveIters);
406
+ var pz = this._resolveCapsuleCollisions(new pc.Vec3(p.x,p.y,p.z-probe), this.maxResolveIters);
407
+
408
+ function d(a,b){ return vLen(vSub(a,b)); }
409
+ var dx = d(nx,base) - d(px,base);
410
+ var dy = d(ny,base) - d(py,base);
411
+ var dz = d(nz,base) - d(pz,base);
412
+ var n = new pc.Vec3(dx,dy,dz);
413
+ var L = vLen(n);
414
+ if (L < 1e-5) return null;
415
+ return vScale(n, 1/L);
416
+ };
417
 
418
+ // Step-up : monter une marche (jusqu'à stepHeight)
419
+ FreeCamera.prototype._tryStepUp = function (from, wishDelta) {
420
+ var R = Math.max(0, this.capsuleRadius);
421
+ var H = Math.max(2*R, this.capsuleHeight);
422
+ var eps = Math.max(1e-4, this.collisionEps);
423
+ var maxH = Math.max(0, this.stepHeight || 0.35);
424
+ var ahead = Math.max(0.05, this.stepAhead || 0.20);
425
+
426
+ var horiz = new pc.Vec3(wishDelta.x, 0, wishDelta.z);
427
+ var hLen = vLen(horiz);
428
+ if (hLen < 1e-6) return null;
429
+ horiz = vScale(horiz, 1/hLen);
430
+
431
+ // on avance un peu, on monte, puis on applique le delta complet
432
+ var probe = vAdd(from, vScale(horiz, Math.min(hLen, ahead)));
433
+ // tester plusieurs hauteurs jusqu'à maxH
434
+ var trials = 3;
435
+ for (var i=1;i<=trials;i++){
436
+ var up = maxH * (i/trials);
437
+ var raised = new pc.Vec3(probe.x, probe.y + up, probe.z);
438
+ raised = this._resolveCapsuleCollisions(raised, this.maxResolveIters);
439
+ var stepped = this._resolveCapsuleCollisions(vAdd(raised, wishDelta), this.maxResolveIters);
440
+
441
+ // validé si on a gagné horizontalement
442
+ if (vLen(vSub(stepped, raised)) > 0.02) return stepped;
443
+ }
444
+ return null;
445
+ };
446
 
447
+ // Snap-down : redéposer sur le sol si on flotte un peu (descente d'escalier)
448
+ FreeCamera.prototype._snapDown = function (p) {
449
+ var R = Math.max(0, this.capsuleRadius);
450
+ var H = Math.max(2*R, this.capsuleHeight);
451
+ var maxDown = Math.max(0, this.snapDownMax || 0.6);
452
+
453
+ // On descend par petits incréments pour “chercher” le sol
454
+ var steps = 4;
455
+ var step = maxDown / steps;
456
+ var cur = p.clone();
457
+ for (var i=0;i<steps;i++){
458
+ var down = new pc.Vec3(cur.x, cur.y - step, cur.z);
459
+ var resolved = this._resolveCapsuleCollisions(down, this.maxResolveIters);
460
+ if (vLen(vSub(resolved, down)) > 0) {
461
+ // on a tapé quelque chose : remonter légèrement et arrêter
462
+ return this._resolveCapsuleCollisions(new pc.Vec3(resolved.x, resolved.y + 0.01, resolved.z), this.maxResolveIters);
463
+ }
464
+ cur.copy(down);
465
+ }
466
+ return p;
467
+ };
468
 
469
+ // ===================== INPUT SOURIS =====================
470
+ var FreeCameraInputMouse = pc.createScript('orbitCameraInputMouse');
471
+ FreeCameraInputMouse.attributes.add('lookSensitivity', { type: 'number', default: 0.3, title: 'Look Sensitivity' });
472
+ FreeCameraInputMouse.attributes.add('wheelSensitivity',{ type: 'number', default: 1.0, title: 'Wheel Sensitivity' });
473
 
474
+ FreeCameraInputMouse.prototype.initialize = function () {
475
+ this.freeCam = this.entity.script.orbitCamera;
476
+ this.last = new pc.Vec2();
477
+ this.isLooking = false;
478
 
479
+ if (this.app.mouse) {
480
+ this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
481
+ this.app.mouse.on(pc.EVENT_MOUSEUP, this.onMouseUp, this);
482
+ this.app.mouse.on(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
483
+ this.app.mouse.on(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
484
+ this.app.mouse.disableContextMenu();
485
  }
486
+ var self = this;
487
+ this._onOut = function(){ self.isLooking = false; };
488
+ window.addEventListener('mouseout', this._onOut, false);
489
+
490
+ this.on('destroy', () => {
491
+ if (this.app.mouse) {
492
+ this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onMouseDown, this);
493
+ this.app.mouse.off(pc.EVENT_MOUSEUP, this.onMouseUp, this);
494
+ this.app.mouse.off(pc.EVENT_MOUSEMOVE, this.onMouseMove, this);
495
+ this.app.mouse.off(pc.EVENT_MOUSEWHEEL, this.onMouseWheel, this);
496
+ }
497
+ window.removeEventListener('mouseout', this._onOut, false);
498
+ });
499
+ };
500
 
501
+ FreeCameraInputMouse.prototype.onMouseDown = function (e) {
502
+ this.isLooking = true;
503
+ this.last.set(e.x, e.y);
504
+ };
505
+
506
+ FreeCameraInputMouse.prototype.onMouseUp = function () {
507
+ this.isLooking = false;
508
+ };
509
 
510
+ FreeCameraInputMouse.prototype.onMouseMove = function (e) {
511
+ if (!this.isLooking || !this.freeCam) return;
512
+ var sens = this.lookSensitivity;
513
+ this.freeCam.yaw = this.freeCam.yaw - e.dx * sens;
514
+ this.freeCam.pitch = this.freeCam.pitch - e.dy * sens;
515
+ this.last.set(e.x, e.y);
516
  };
517
 
518
+ FreeCameraInputMouse.prototype.onMouseWheel = function (e) {
519
+ if (!this.freeCam) return;
520
+ var cam = this.entity;
521
+ var move = -e.wheelDelta * this.wheelSensitivity * this.freeCam.dollySpeed * 0.05;
522
+ var fwd = cam.forward.clone(); if (fwd.lengthSq()>1e-8) fwd.normalize();
523
+
524
+ var from = cam.getPosition().clone();
525
+ var to = from.clone().add(fwd.mulScalar(move));
526
+ var next = this.freeCam._moveSwept(from, to.sub(from));
527
+ this.freeCam._clampBBoxMinY(next);
528
+ cam.setPosition(next);
529
+
530
+ e.event.preventDefault();
531
  };
532
 
533
+ // ===================== INPUT TOUCH =====================
534
+ var FreeCameraInputTouch = pc.createScript('orbitCameraInputTouch');
535
+ FreeCameraInputTouch.attributes.add('lookSensitivity', { type: 'number', default: 0.5, title: 'Look Sensitivity' });
536
+ FreeCameraInputTouch.attributes.add('pinchDollyFactor', { type: 'number', default: 0.02, title: 'Pinch Dolly Factor' });
537
+
538
+ FreeCameraInputTouch.prototype.initialize = function () {
539
+ this.freeCam = this.entity.script.orbitCamera;
540
+ this.last = new pc.Vec2();
541
+ this.isLooking = false;
542
+ this.lastPinch = 0;
543
+
544
+ if (this.app.touch) {
545
+ this.app.touch.on(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
546
+ this.app.touch.on(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
547
+ this.app.touch.on(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
548
+ this.app.touch.on(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
549
+ }
550
 
551
+ this.on('destroy', () => {
552
+ if (this.app.touch) {
553
+ this.app.touch.off(pc.EVENT_TOUCHSTART, this.onTouchStartEndCancel, this);
554
+ this.app.touch.off(pc.EVENT_TOUCHEND, this.onTouchStartEndCancel, this);
555
+ this.app.touch.off(pc.EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this);
556
+ this.app.touch.off(pc.EVENT_TOUCHMOVE, this.onTouchMove, this);
557
+ }
558
+ });
559
  };
560
 
561
+ FreeCameraInputTouch.prototype.onTouchStartEndCancel = function (e) {
 
562
  var t = e.touches;
563
  if (t.length === 1) {
564
+ this.isLooking = (e.event.type === 'touchstart');
565
+ this.last.set(t[0].x, t[0].y);
566
  } else if (t.length === 2) {
567
  var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
568
+ this.lastPinch = Math.sqrt(dx*dx + dy*dy);
 
569
  } else {
570
+ this.isLooking = false;
571
  }
572
  };
573
 
574
+ FreeCameraInputTouch.prototype.onTouchMove = function (e) {
575
  var t = e.touches;
576
+ if (!this.freeCam) return;
577
+
578
+ if (t.length === 1 && this.isLooking) {
579
+ var s = this.lookSensitivity;
580
+ var dx = t[0].x - this.last.x, dy = t[0].y - this.last.y;
581
+ this.freeCam.yaw = this.freeCam.yaw - dx * s;
582
+ this.freeCam.pitch = this.freeCam.pitch - dy * s;
583
+ this.last.set(t[0].x, t[0].y);
584
  } else if (t.length === 2) {
585
  var dx = t[0].x - t[1].x, dy = t[0].y - t[1].y;
586
  var dist = Math.sqrt(dx*dx + dy*dy);
587
+ var delta = dist - this.lastPinch; this.lastPinch = dist;
588
+
589
+ var cam = this.entity;
590
+ var fwd = cam.forward.clone(); if (fwd.lengthSq()>1e-8) fwd.normalize();
591
+ var from = cam.getPosition().clone();
592
+ var to = from.clone().add(fwd.mulScalar(delta * this.pinchDollyFactor * this.freeCam.dollySpeed));
593
+ var next = this.freeCam._moveSwept(from, to.sub(from));
594
+ this.freeCam._clampBBoxMinY(next);
595
+ cam.setPosition(next);
596
  }
597
  };
598
 
599
+ // ===================== INPUT CLAVIER =====================
600
+ var FreeCameraInputKeyboard = pc.createScript('orbitCameraInputKeyboard');
601
+ FreeCameraInputKeyboard.attributes.add('acceleration', { type: 'number', default: 1.0, title: 'Accel (unused, future)' });
602
 
603
+ FreeCameraInputKeyboard.prototype.initialize = function () {
604
+ this.freeCam = this.entity.script.orbitCamera;
605
+ this.kb = this.app.keyboard || null;
606
  };
607
 
608
+ FreeCameraInputKeyboard.prototype.update = function (dt) {
609
+ if (!this.freeCam || !this.kb) return;
610
 
611
+ var fwd = (this.kb.isPressed(pc.KEY_UP) || this.kb.isPressed(pc.KEY_Z)) ? 1 :
612
+ (this.kb.isPressed(pc.KEY_DOWN) || this.kb.isPressed(pc.KEY_S)) ? -1 : 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
+ var strf = (this.kb.isPressed(pc.KEY_RIGHT) || this.kb.isPressed(pc.KEY_D)) ? 1 :
615
+ (this.kb.isPressed(pc.KEY_LEFT) || this.kb.isPressed(pc.KEY_Q)) ? -1 : 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
 
617
+ if (fwd !== 0 || strf !== 0) {
618
+ var cam = this.entity;
619
+ var from = cam.getPosition().clone();
620
+
621
+ var forward = cam.forward.clone(); if (forward.lengthSq()>1e-8) forward.normalize();
622
+ var right = cam.right.clone(); if (right.lengthSq()>1e-8) right.normalize();
623
 
624
+ var delta = new pc.Vec3()
625
+ .add(forward.mulScalar(fwd * this.freeCam.moveSpeed * dt))
626
+ .add(right .mulScalar(strf * this.freeCam.strafeSpeed * dt));
627
+
628
+ var next = this.freeCam._moveSwept(from, delta);
629
+ this.freeCam._clampBBoxMinY(next);
630
+ cam.setPosition(next);
631
  }
 
632
 
633
+ // Rotation clavier (Shift + flèches)
634
+ var shift = this.kb.isPressed(pc.KEY_SHIFT);
635
+ if (shift) {
636
+ var yawDir = (this.kb.isPressed(pc.KEY_LEFT) ? 1 : 0) - (this.kb.isPressed(pc.KEY_RIGHT) ? 1 : 0);
637
+ var pitchDir = (this.kb.isPressed(pc.KEY_UP) ? 1 : 0) - (this.kb.isPressed(pc.KEY_DOWN) ? 1 : 0);
638
+ var yawSpeed = 120, pitchSpeed = 90;
639
+ if (yawDir !== 0) this.freeCam.yaw = this.freeCam.yaw + yawDir * yawSpeed * dt;
640
+ if (pitchDir !== 0) this.freeCam.pitch = this.freeCam.pitch + pitchDir * pitchSpeed * dt;
641
+ }
642
+ };