MikaFil commited on
Commit
c1fd297
·
verified ·
1 Parent(s): 3508121

Update deplacement_dans_env/ctrl_camera_pr_env.js

Browse files
deplacement_dans_env/ctrl_camera_pr_env.js CHANGED
@@ -1,642 +1,404 @@
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
  };
 
 
 
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; };