tosijs-3d 0.6.2 → 0.7.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/b3d-aircraft.d.ts +79 -2
  2. package/dist/b3d-aircraft.d.ts.map +1 -1
  3. package/dist/b3d-aircraft.js +349 -17
  4. package/dist/b3d-aircraft.js.map +1 -1
  5. package/dist/b3d-hud.d.ts +11 -0
  6. package/dist/b3d-hud.d.ts.map +1 -1
  7. package/dist/b3d-hud.js +20 -0
  8. package/dist/b3d-hud.js.map +1 -1
  9. package/dist/b3d-library.d.ts.map +1 -1
  10. package/dist/b3d-library.js +20 -6
  11. package/dist/b3d-library.js.map +1 -1
  12. package/dist/b3d-patch.d.ts +137 -0
  13. package/dist/b3d-patch.d.ts.map +1 -0
  14. package/dist/b3d-patch.js +665 -0
  15. package/dist/b3d-patch.js.map +1 -0
  16. package/dist/b3d-terrain.d.ts +63 -3
  17. package/dist/b3d-terrain.d.ts.map +1 -1
  18. package/dist/b3d-terrain.js +177 -11
  19. package/dist/b3d-terrain.js.map +1 -1
  20. package/dist/b3d-utils.d.ts +13 -0
  21. package/dist/b3d-utils.d.ts.map +1 -1
  22. package/dist/b3d-utils.js +42 -0
  23. package/dist/b3d-utils.js.map +1 -1
  24. package/dist/biome-plugin.d.ts +26 -0
  25. package/dist/biome-plugin.d.ts.map +1 -1
  26. package/dist/biome-plugin.js +42 -8
  27. package/dist/biome-plugin.js.map +1 -1
  28. package/dist/carve.d.ts +89 -0
  29. package/dist/carve.d.ts.map +1 -0
  30. package/dist/carve.js +223 -0
  31. package/dist/carve.js.map +1 -0
  32. package/dist/control-input.d.ts +5 -0
  33. package/dist/control-input.d.ts.map +1 -1
  34. package/dist/control-input.js +5 -0
  35. package/dist/control-input.js.map +1 -1
  36. package/dist/fly-by-wire.d.ts +45 -0
  37. package/dist/fly-by-wire.d.ts.map +1 -1
  38. package/dist/fly-by-wire.js +116 -8
  39. package/dist/fly-by-wire.js.map +1 -1
  40. package/dist/glass-gamepad.d.ts +21 -0
  41. package/dist/glass-gamepad.d.ts.map +1 -1
  42. package/dist/glass-gamepad.js +70 -0
  43. package/dist/glass-gamepad.js.map +1 -1
  44. package/dist/hud-math.d.ts +13 -0
  45. package/dist/hud-math.d.ts.map +1 -1
  46. package/dist/hud-math.js +40 -0
  47. package/dist/hud-math.js.map +1 -1
  48. package/dist/hud.d.ts +24 -0
  49. package/dist/hud.d.ts.map +1 -1
  50. package/dist/hud.js +62 -2
  51. package/dist/hud.js.map +1 -1
  52. package/dist/index.d.ts +11 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +8 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/landform.d.ts +92 -0
  57. package/dist/landform.d.ts.map +1 -1
  58. package/dist/landform.js +110 -0
  59. package/dist/landform.js.map +1 -1
  60. package/dist/patch-field.d.ts +38 -0
  61. package/dist/patch-field.d.ts.map +1 -0
  62. package/dist/patch-field.js +92 -0
  63. package/dist/patch-field.js.map +1 -0
  64. package/dist/sdf-lattice.d.ts +69 -0
  65. package/dist/sdf-lattice.d.ts.map +1 -0
  66. package/dist/sdf-lattice.js +254 -0
  67. package/dist/sdf-lattice.js.map +1 -0
  68. package/dist/terrain-grid.d.ts +73 -0
  69. package/dist/terrain-grid.d.ts.map +1 -1
  70. package/dist/terrain-grid.js +151 -1
  71. package/dist/terrain-grid.js.map +1 -1
  72. package/dist/tosi-b3d.d.ts +17 -0
  73. package/dist/tosi-b3d.d.ts.map +1 -1
  74. package/dist/tosi-b3d.js +29 -0
  75. package/dist/tosi-b3d.js.map +1 -1
  76. package/dist/virtual-gamepad.d.ts.map +1 -1
  77. package/dist/virtual-gamepad.js +11 -4
  78. package/dist/virtual-gamepad.js.map +1 -1
  79. package/package.json +1 -1
@@ -12,13 +12,40 @@ export declare class B3dAircraft extends B3dControllable {
12
12
  player: boolean;
13
13
  enterable: boolean;
14
14
  ceiling: number;
15
- hudChase: boolean;
15
+ hudChaseOff: boolean;
16
16
  hudSize: number;
17
17
  hudForward: number;
18
18
  maxSpeed: number;
19
19
  afterburnerSpeed: number;
20
20
  acceleration: number;
21
21
  vtolSpeed: number;
22
+ /** How fast the craft may back up in hover (units/s). */
23
+ reverseSpeed: number;
24
+ /** How fast the trigger moves the throttle lever (setting/sec). */
25
+ throttleRate: number;
26
+ /**
27
+ * AUTO LANDING GEAR. `'on'` finds the model's gear-retract AnimationGroups
28
+ * by name and runs them with height above ground: up on climb-out, down on
29
+ * approach. `'off'` leaves the gear to you (call `setGear`).
30
+ */
31
+ autoGear: "on" | "off";
32
+ /** Height above ground (m) at which the gear retracts. It extends again at
33
+ * 60% of this — hysteresis, so a bumpy approach doesn't cycle it. */
34
+ gearAltitude: number;
35
+ /** Optional sound for the gear cycle (URL). Played spatially at the
36
+ * airframe, once per transition. */
37
+ gearSound: string;
38
+ /** Volume for `gearSound`. */
39
+ gearVolume: number;
40
+ /** Seconds for a full gear cycle. */
41
+ gearTime: number;
42
+ /** How far the LOOK stick can swing the view (degrees each way). */
43
+ lookRange: number;
44
+ /** Look slew rate (degrees/sec at full deflection). */
45
+ lookRate: number;
46
+ /** How fast the view springs back to centre when the stick is released
47
+ * (fraction of the remaining offset per second). */
48
+ lookReturn: number;
22
49
  hoverCeiling: number;
23
50
  groundY: number;
24
51
  crashSpeed: number;
@@ -77,7 +104,7 @@ export declare class B3dAircraft extends B3dControllable {
77
104
  private fbw;
78
105
  private fbwSeeded;
79
106
  ceiling: number;
80
- hudChase: boolean;
107
+ hudChaseOff: boolean;
81
108
  reticle: string;
82
109
  reticleRange: number;
83
110
  private _hud;
@@ -87,9 +114,16 @@ export declare class B3dAircraft extends B3dControllable {
87
114
  private meshNode;
88
115
  private _chasePivot;
89
116
  private _chaseLookPitch;
117
+ /** Where the LOOK stick has swung the view (radians), and how fast it
118
+ * springs back when released. */
119
+ private _lookYaw;
120
+ private _lookPitch;
90
121
  private meshesToDispose;
91
122
  private _lastGroundDist;
92
123
  private _groundNormal;
124
+ /** True while the airframe is in open air INSIDE the ground (a bore/cavern):
125
+ * heightfield assumptions are suspended for the frame. */
126
+ private _inCavity;
93
127
  private _worldVel;
94
128
  private _prevPos;
95
129
  private _prevPosValid;
@@ -130,6 +164,49 @@ export declare class B3dAircraft extends B3dControllable {
130
164
  private get gunWarhead();
131
165
  /** Nearest destroyable within `range` and inside the forward cone (or null). */
132
166
  private acquireTarget;
167
+ /** The model's gear-retract animations, found by name (see `_findGear`). */
168
+ private _gearGroups;
169
+ /** true = retracted (or retracting). Starts DOWN: you spawn on the ground. */
170
+ private _gearUp;
171
+ private _gearSound;
172
+ /** 0 = down, 1 = up — scrubbed toward `_gearTarget`. */
173
+ private _gearPos;
174
+ private _gearTarget;
175
+ /**
176
+ * Find the gear animations on a freshly-loaded model, by NAME.
177
+ *
178
+ * Convention over configuration: a group whose name mentions "gear" and
179
+ * "retract" (or "up") is a landing-gear animation, so the scout's
180
+ * "Main Gear (L) Retract" / "Nose Gear Retract" are picked up with no
181
+ * authoring beyond what's already in the GLB. Library instances carry their
182
+ * groups on `metadata.animationGroups` (renamed per instance), so several
183
+ * aircraft each animate their own gear.
184
+ */
185
+ private _findGear;
186
+ /**
187
+ * Raise or lower the gear. Public so an AI pilot, a cutscene or a key bind
188
+ * can call it; `autoGear` drives it from altitude otherwise.
189
+ */
190
+ setGear(up: boolean): void;
191
+ /**
192
+ * The gear is SCRUBBED, not played.
193
+ *
194
+ * Letting the AnimationGroup play itself looked obvious and doesn't work:
195
+ * glTF animations arrive with a cyclic loop mode, so a group told to stop at
196
+ * the end can snap back to frame 0 — the gear cycles and then vanishes
197
+ * (exactly what Tonio saw) — and reverse playback via `start(from > to)` is
198
+ * unreliable across Babylon versions. Advancing a normalised position and
199
+ * calling `goToFrame` sidesteps all of it: no loop mode, no end-of-group
200
+ * behaviour, no second animation to author for the reverse, and an
201
+ * interrupted cycle simply turns around from wherever it had got to.
202
+ */
203
+ private _scrubGear;
204
+ /** True while the gear is up (or on its way). */
205
+ get gearUp(): boolean;
206
+ private _playGearSound;
207
+ /** Drive the gear from height above ground, with hysteresis so a bumpy
208
+ * approach or a hill passing underneath doesn't cycle it. */
209
+ private _updateGear;
133
210
  /** Distance from the aircraft origin down to the nearest ground: the lower of
134
211
  * any terrain collider the raycast hits and the configured ground plane. */
135
212
  private groundDistance;
@@ -1 +1 @@
1
- {"version":3,"file":"b3d-aircraft.d.ts","sourceRoot":"","sources":["../src/b3d-aircraft.ts"],"names":[],"mappings":"AAiOA,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAC1C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAYnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AA+D3C,qBAAa,WAAY,SAAQ,eAAe;IAC9C,YAAY,2CAAoB;IAEhC,MAAM,CAAC,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAsDpB;IAGD,QAAQ,SAAI;IACZ,QAAQ,SAAI;IACZ,aAAa,SAAI;IACjB,UAAU,UAAQ;IAClB,QAAQ,UAAQ;IAChB,MAAM,UAAQ;IACd,QAAQ,UAAQ;IAChB,OAAO,UAAQ;IACf,8FAA8F;IAC9F,OAAO,CAAC,SAAS,CAAQ;IACzB;kEAC8D;IAC9D,UAAU,EAAE,OAAO,GAAG,SAAS,CAAU;IACzC,OAAO,CAAC,cAAc,CAAQ;IAE9B;;;wEAGoE;IACpE,SAAS,SAAM;IACf,cAAc,SAAM;IACpB,cAAc,SAAM;IACpB,WAAW,SAAM;IACjB,aAAa,SAAM;IAEnB,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,IAAI,CAAwB;IAEpC,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,WAAW,CAAQ;IAG3B,OAAO,CAAC,GAAG,CAA8D;IACzE,OAAO,CAAC,SAAS,CAAQ;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IAE5B,OAAO,CAAC,IAAI,CAAwC;IACpD,OAAO,CAAC,WAAW,CAAQ;IAG3B,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,QAAQ,CAAqC;IAKrD,OAAO,CAAC,WAAW,CAAqC;IACxD,OAAO,CAAC,eAAe,CAAI;IAC3B,OAAO,CAAC,eAAe,CAAqB;IAM5C,OAAO,CAAC,eAAe,CAAW;IAClC,OAAO,CAAC,aAAa,CAA+B;IAMpD,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,IAAI,CAIX;IACD,OAAO,CAAC,UAAU,CAAyC;IAG3D,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,WAAW,CAA4B;IAE/C,eAAe,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI;IAItC,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM;IA6T1C,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,QAAQ,CAA2B;IAE3C,gFAAgF;IAChF,OAAO,CAAC,eAAe;IA8BvB,mEAAmE;IACnE,IAAI,KAAK,IAAI,QAAQ,GAAG,IAAI,CAM3B;IAED;8EAC0E;IAC1E,OAAO,CAAC,SAAS;IAUjB;;;;0DAIsD;IACtD,OAAO,CAAC,MAAM;IAUd,yEAAyE;IACzE,QAAQ,IAAI,IAAI;IAmBhB;;0CAEsC;IACtC,QAAQ,IAAI,IAAI;IAgBhB;;;;;OAKG;IACH,WAAW,IAAI,IAAI;IAuDnB,OAAO,KAAK,UAAU,GAMrB;IAED,gFAAgF;IAChF,OAAO,CAAC,aAAa;IAyBrB;gFAC4E;IAC5E,OAAO,CAAC,cAAc;IAMtB,+EAA+E;IAC/E,OAAO,CAAC,KAAK;IAQb;;qDAEiD;IACjD,OAAO,CAAC,aAAa;IAqCrB,OAAO,CAAC,YAAY;IASpB,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK;IAW3C,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,eAAe;IAwCvB,OAAO,CAAC,SAAS;IAsCjB;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IA8BtB,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,aAAa,CAAkC;IAEvD,iBAAiB;IA6EjB;;qCAEiC;IACjC,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS;IAQvC,YAAY;CAuCb;AAED,eAAO,MAAM,WAAW,8CAEtB,CAAA"}
1
+ {"version":3,"file":"b3d-aircraft.d.ts","sourceRoot":"","sources":["../src/b3d-aircraft.ts"],"names":[],"mappings":"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"}
@@ -22,7 +22,8 @@ const MAX_ALT = 300 // the aircraft's ceiling (its `ceiling`, default 300)
22
22
 
23
23
  // A FACTORY — so a respawn is a genuinely new aircraft (with its radar), not a reset. The sim
24
24
  // really emits a death and a spawn, which is the stream a narrative driver reads (see b3d-death).
25
- // The HUD stays in the COCKPIT view only (hudChase defaults false); the chase view is clean.
25
+ // The HUD shows in BOTH views: in-scene on the canopy in cockpit, flat overlay in chase
26
+ // (minus the artificial horizon, which only tells the truth from inside the aircraft).
26
27
  const plane = () => b3dAircraft(
27
28
  { library: 'vehicles', meshName: 'scout', player: true, y: 0, vtolSpeed: 6, maxSpeed: 55 },
28
29
  b3dRadar({ range: RADAR_RANGE, coneDeg: 90, lockTime: 1.2, maxLocks: 2 }),
@@ -175,10 +176,19 @@ lift/throttle (the dual-purpose axis above), right stick Y = camera zoom.
175
176
  | `afterburnerSpeed` | `75` | Speed ceiling while the throttle is held past `maxSpeed`; releasing bleeds back to `maxSpeed`. ≤ `maxSpeed` disables afterburner. |
176
177
  | `acceleration` | `12` | Throttle / lean authority (speed change rate) |
177
178
  | `vtolSpeed` | `6` | Forward ground speed splitting hover (below) from plane (above). 0 = pure aeroplane, no hover regime. |
178
- | `hoverCeiling` | `50` | Height above ground above which the trigger is forward thrust regardless of speed (take off vertically, then fly) and the brake can't stall you below `vtolSpeed`. Below it, slowing to a hover gives the vertical trigger back for a vertical landing. 0 = off. |
179
+ | `lookRange` | `120` | How far the right stick can swing the view (degrees each way) |
180
+ | `lookRate` | `150` | Look slew rate (degrees/sec at full stick) |
181
+ | `lookReturn` | `4` | How fast the view springs back to centre when released |
182
+ | `autoGear` | `'on'` | Find the model's gear-retract animations by NAME and run them from height above ground. `'off'` = manual (`setGear(up)`) |
183
+ | `gearAltitude` | `40` | Height above ground (m) at which the gear retracts; it extends again at 60% of this (hysteresis) |
184
+ | `gearTime` | `2.5` | Seconds for a full gear cycle |
185
+ | `gearSound` | `''` | Optional URL played spatially at the airframe on each gear transition |
186
+ | `gearVolume` | `0.6` | Volume for `gearSound` |
187
+ | `afterburnerSpeed` (behaviour) | — | Reached only while the trigger is HELD past the detent at full lever; release and you settle back to `maxSpeed` (military) |
188
+ | `hoverCeiling` | `140` | Height above ground above which the trigger is forward thrust regardless of speed (take off vertically, then fly) and the brake can't stall you below `vtolSpeed`. Below it, slowing to a hover gives the vertical trigger back for a vertical landing. 0 = off. |
179
189
  | `groundY` | `0` | Assumed ground-plane height (a floor in addition to any terrain colliders) |
180
190
  | `crashSpeed` | `8` | Vertical impact speed (m/s) above which a ground contact is a crash |
181
- | `hudChase` | `false` | Show the flat DOM HUD overlay in chase view (cockpit uses the in-scene HUD) |
191
+ | `hudChaseOff` | `false` | Hide the HUD entirely in chase view. By default chase shows the HUD **without the artificial horizon** (which would contradict the real one behind the aircraft); cockpit shows everything, in-scene |
182
192
  | `hudSize` | `0.7` | In-cockpit HUD plane size (metres) |
183
193
  | `hudForward` | `1.6` | How far ahead of the pilot's eye the HUD floats (metres) |
184
194
  | `weapons` | `'on'` | `'off'` disarms all weapons |
@@ -208,6 +218,52 @@ Shells inherit the airframe's velocity, so your own motion leads the shot. Any
208
218
  `fireGuns()`, `dropBomb()`, and `fireMissile()` are also callable directly (e.g. for
209
219
  an AI pilot). Set `weapons="off"` to disarm.
210
220
 
221
+ ## The throttle is a LEVER, and the HUD marks where it's taking you
222
+
223
+ Full lever = **military** thrust, the fastest speed you can simply leave set.
224
+ **Afterburner is held, not parked**: it lights only while the trigger is past
225
+ a detent with the lever already at full, and letting go settles you back to
226
+ military speed rather than cruising in reheat.
227
+
228
+ Because a lever commands an *equilibrium*, the needle is always travelling
229
+ toward a number rather than sitting on one — which looks like a fault if the
230
+ gauge doesn't show the target. The HUD therefore marks it (`setMeterMarks`),
231
+ and the same mechanism marks **sea level** on the altimeter when you drop
232
+ below it and **the ground beneath you** when that's higher: an altimeter reads
233
+ height above datum, which is the wrong number exactly when terrain is the
234
+ thing about to hit you.
235
+
236
+ ## The right stick is the CAMERA
237
+
238
+ It swings the view and **springs back** when you let go: in chase it orbits the
239
+ aircraft, in the cockpit it turns the pilot's head. Held, it slews; released,
240
+ it returns — so you can glance at what you're about to hit without leaving the
241
+ camera somewhere awkward, and without a second control to re-centre.
242
+
243
+ It used to be an aux roll axis, which was near-useless: the left stick already
244
+ banks, and bank-to-turn means a second roll input fights it. Looking around is
245
+ what the spare stick is actually for.
246
+
247
+ ## Landing gear, found by name
248
+
249
+ If the model carries AnimationGroups whose names mention **gear** and
250
+ **retract** (the scout's `Main Gear (L) Retract`, `Nose Gear Retract`, …), the
251
+ aircraft finds them and runs them with height above ground — up on climb-out,
252
+ down on approach, with hysteresis so a bumpy approach doesn't cycle them.
253
+ Nothing to wire: it's convention over configuration, and a model without such
254
+ animations simply has no gear to work.
255
+
256
+ The animation is **scrubbed, not played** — a normalised position is advanced
257
+ each frame and pushed with `goToFrame`. Playing the group looked simpler and
258
+ failed: glTF animations arrive with a cyclic loop mode, so a group told to stop
259
+ at the end can snap back to frame 0 (the gear cycles, then vanishes), and
260
+ reversing via `start(from > to)` is unreliable. Scrubbing also means an
261
+ interrupted cycle turns around from wherever it got to, and the SAME animation
262
+ serves both directions — there's no second one to author or keep in sync.
263
+
264
+ `gearSound` plays spatially at the airframe on each transition;
265
+ `setGear(up)` and `gearUp` are public for an AI pilot or a key bind.
266
+
211
267
  ## API (read-only properties for HUD binding)
212
268
 
213
269
  - `airspeed: number` — current forward speed (m/s)
@@ -226,7 +282,7 @@ import * as BABYLON from '@babylonjs/core';
226
282
  import { canonicalize, applyCenterOfGravity } from './model-transform';
227
283
  import { B3dControllable } from './b3d-controllable';
228
284
  import { aircraftMapping } from './virtual-gamepad';
229
- import { flyByWireStep, targetVelocity, chaseVelocity, } from './fly-by-wire';
285
+ import { equilibriumSpeed, flyByWireStep, targetVelocity, chaseVelocity, } from './fly-by-wire';
230
286
  import { placeOnSurface, boundingBottomOffset, isOff } from './b3d-utils';
231
287
  import { spawnProjectile, spawnMissile } from './b3d-launcher';
232
288
  // Small gap kept between the model's belly and the ground.
@@ -278,7 +334,8 @@ export class B3dAircraft extends B3dControllable {
278
334
  // a linked HUD's altitude gauge.
279
335
  ceiling: 300,
280
336
  // Show the HUD in the chase view too (default: cockpit view only).
281
- hudChase: false,
337
+ // Chase view shows the HUD MINUS the horizon; set to hide it entirely.
338
+ hudChaseOff: false,
282
339
  // In-cockpit HUD plane placement (see b3d-hud attachInScene): its size in metres
283
340
  // and how far ahead of the pilot's eye it floats. Tune to taste per airframe.
284
341
  hudSize: 0.7,
@@ -292,11 +349,44 @@ export class B3dAircraft extends B3dControllable {
292
349
  // up/down) and above which it flies like a plane (triggers = throttle). Set
293
350
  // to 0 for a pure aeroplane with no hover regime.
294
351
  vtolSpeed: 6,
352
+ /** How fast the craft may back up in hover (units/s). */
353
+ reverseSpeed: 5,
354
+ /** How fast the trigger moves the throttle lever (setting/sec). */
355
+ throttleRate: 0.8,
356
+ /**
357
+ * AUTO LANDING GEAR. `'on'` finds the model's gear-retract AnimationGroups
358
+ * by name and runs them with height above ground: up on climb-out, down on
359
+ * approach. `'off'` leaves the gear to you (call `setGear`).
360
+ */
361
+ autoGear: 'on',
362
+ /** Height above ground (m) at which the gear retracts. It extends again at
363
+ * 60% of this — hysteresis, so a bumpy approach doesn't cycle it. */
364
+ gearAltitude: 40,
365
+ /** Optional sound for the gear cycle (URL). Played spatially at the
366
+ * airframe, once per transition. */
367
+ gearSound: '',
368
+ /** Volume for `gearSound`. */
369
+ gearVolume: 0.6,
370
+ /** Seconds for a full gear cycle. */
371
+ gearTime: 2.5,
372
+ /** How far the LOOK stick can swing the view (degrees each way). */
373
+ lookRange: 120,
374
+ /** Look slew rate (degrees/sec at full deflection). */
375
+ lookRate: 150,
376
+ /** How fast the view springs back to centre when the stick is released
377
+ * (fraction of the remaining offset per second). */
378
+ lookReturn: 4,
295
379
  // Height above ground above which the trigger is forward thrust regardless of
296
380
  // speed (take off vertically, then fly) AND the brake can't stall you below
297
381
  // vtolSpeed. Below it, slowing to a hover gives the vertical trigger back for a
298
382
  // vertical landing. 0 = altitude gate off (regime is speed-only).
299
- hoverCeiling: 50,
383
+ // 50m put the hover regime out of reach in normal flight over broken
384
+ // ground: you had to descend into a valley before the brake could stall
385
+ // you, which reads as "the brake doesn't work" (Tonio diagnosed it as
386
+ // altitude-driven). 140m keeps the intent — you can't decelerate into a
387
+ // hover from cruise ALTITUDE — while making hovering reachable wherever
388
+ // you'd actually want it.
389
+ hoverCeiling: 140,
300
390
  // Assumed ground-plane height (used as a floor in addition to any terrain
301
391
  // colliders the downward raycast hits).
302
392
  groundY: 0,
@@ -370,7 +460,11 @@ export class B3dAircraft extends B3dControllable {
370
460
  // dragging the camera with it. The airframe's small attitude jitter, amplified by the ~5m chase
371
461
  // lever arm, was the whole reason the chase was jittery while the pivot-adjacent cockpit wasn't.
372
462
  _chasePivot = null;
373
- _chaseLookPitch = 0; // fixed look-down angle of the chase camera
463
+ _chaseLookPitch = 0;
464
+ /** Where the LOOK stick has swung the view (radians), and how fast it
465
+ * springs back when released. */
466
+ _lookYaw = 0;
467
+ _lookPitch = 0; // fixed look-down angle of the chase camera
374
468
  meshesToDispose = [];
375
469
  // Ground sampling is ONE raycast per frame, taken after the move and cached: the
376
470
  // pre-move regime height reuses last frame's value (one-frame stale, like the
@@ -379,6 +473,9 @@ export class B3dAircraft extends B3dControllable {
379
473
  // Set, and a child-mesh array three times a frame.
380
474
  _lastGroundDist = Infinity;
381
475
  _groundNormal = new BABYLON.Vector3(0, 1, 0);
476
+ /** True while the airframe is in open air INSIDE the ground (a bore/cavern):
477
+ * heightfield assumptions are suspended for the frame. */
478
+ _inCavity = false;
382
479
  // TRUE world velocity, tracked from frame displacement. this.velocity is
383
480
  // only the hover/ground integrator and reads ZERO in wing-borne flight
384
481
  // (the fbw path moves the node directly) — weapons inheriting it left
@@ -444,10 +541,13 @@ export class B3dAircraft extends B3dControllable {
444
541
  // that lands banked settles flat.
445
542
  const cmd = {
446
543
  pitch: this.grounded ? 0 : input.pitch,
447
- // Left stick X is the primary turn (banks → turns); right stick X adds roll.
544
+ // Left stick X banks, and bank turns you. `strafe` is still summed in so
545
+ // an ALTERNATIVE mapping can offer a dedicated roll axis — but the
546
+ // aircraft preset no longer puts one on the right stick, which now looks
547
+ // around instead.
448
548
  roll: this.grounded
449
549
  ? 0
450
- : Math.max(-1, Math.min(1, input.turn + input.strafe)),
550
+ : Math.max(-1, Math.min(1, input.turn + (input.strafe ?? 0))),
451
551
  lift: input.lift, // trigger axis: + up/faster, − down/slower
452
552
  };
453
553
  const cfg = {
@@ -465,6 +565,16 @@ export class B3dAircraft extends B3dControllable {
465
565
  // gets you over vtolSpeed and into forward flight quickly (shallow, not a dive).
466
566
  leanAccel: attrs.acceleration * 2,
467
567
  hoverDamp: HOVER_DAMP,
568
+ // Hover: the LEAN reverses you (nose up), the brake half of the trigger
569
+ // sheds speed to a stop, and the throttle half stays purely vertical —
570
+ // so slowing down never fights altitude.
571
+ hoverBrake: attrs.acceleration * 1.5,
572
+ // Plane mode: the trigger moves a THROTTLE LEVER and speed settles where
573
+ // thrust meets drag, so a climb costs speed and a dive gives it back
574
+ // without touching the setting. Past idle the trigger is an airbrake.
575
+ throttleRate: attrs.throttleRate,
576
+ brakeAccel: attrs.acceleration * 1.2,
577
+ reverseSpeed: attrs.reverseSpeed,
468
578
  climbRate: attrs.maxSpeed * 0.3,
469
579
  offLevelSink: attrs.maxSpeed * 0.12,
470
580
  diveBoost: attrs.maxSpeed * 0.4,
@@ -476,7 +586,18 @@ export class B3dAircraft extends B3dControllable {
476
586
  const fwdSpeed = Math.hypot(vel.x, vel.z);
477
587
  // Reuse last frame's post-move ground distance (refined after the move below).
478
588
  // One frame stale, but so is `grounded`, and regime selection is tolerant.
479
- const heightAboveGround = this._lastGroundDist - this.groundClearance;
589
+ //
590
+ // INSIDE A TUNNEL, clearance stops meaning what it means outside. The
591
+ // regime uses height-above-ground to know you're taking off or landing —
592
+ // but in a bore you are DELIBERATELY three metres off the floor at speed,
593
+ // and letting that read as "about to land" drops you out of wing-borne
594
+ // flight in the one place you most need control. So underground the
595
+ // regime is decided by SPEED alone: report a clearance that can't trip the
596
+ // hover threshold, and let the floor still govern the clamp and the
597
+ // landing gate (which run on the real distance below).
598
+ const heightAboveGround = this._inCavity
599
+ ? Number.POSITIVE_INFINITY
600
+ : this._lastGroundDist - this.groundClearance;
480
601
  flyByWireStep(this.fbw, cmd, fwdSpeed, heightAboveGround, cfg, dt, this.grounded);
481
602
  // Realise the attitude as a quaternion. Babylon's +pitch(X) drops the nose
482
603
  // and +roll(Z) banks left, so negate both (our state: +pitch = nose up,
@@ -492,8 +613,12 @@ export class B3dAircraft extends B3dControllable {
492
613
  // Read-only flight state for the HUD / XR rig.
493
614
  this.airspeed = this.fbw.speed;
494
615
  this.altitude = node.position.y;
495
- this.throttleLevel =
496
- attrs.maxSpeed > 0 ? this.fbw.speed / attrs.maxSpeed : 0;
616
+ // 0..100% spans the WHOLE envelope, afterburner included. Dividing by
617
+ // maxSpeed made the meter read 134% in a cruise the aircraft is perfectly
618
+ // happy in — so "it went to maximum" was the gauge pegging, not the
619
+ // aircraft misbehaving (Tonio, 2026-08-13).
620
+ const topSpeed = Math.max(attrs.maxSpeed, attrs.afterburnerSpeed);
621
+ this.throttleLevel = topSpeed > 0 ? this.fbw.speed / topSpeed : 0;
497
622
  // "In VTOL" = slow (below vtolSpeed), regardless of altitude — above the hover
498
623
  // ceiling you can be stalled but the thrust still goes forward.
499
624
  this.vtolActive = attrs.vtolSpeed > 0 && fwdSpeed < attrs.vtolSpeed;
@@ -521,12 +646,42 @@ export class B3dAircraft extends B3dControllable {
521
646
  // chase view only when `hud-chase` opts in.
522
647
  const inCockpit = this.cameraView === 'cockpit';
523
648
  this._hud.setInSceneVisible?.(inCockpit);
524
- const showHud = inCockpit || attrs.hudChase;
525
- this._hud.setVisible(!inCockpit && attrs.hudChase);
649
+ // The HUD is useful from ANY view — speed, altitude, radar, warnings are
650
+ // true wherever the camera is. The horizon is the exception: outside the
651
+ // cockpit it contradicts the real horizon behind the aircraft, so it's
652
+ // dropped in chase and the rest stays. (`hudChase: false` still forces
653
+ // the old cockpit-only behaviour for anyone who wants a clean chase.)
654
+ const chaseHud = !attrs.hudChaseOff;
655
+ const showHud = inCockpit || chaseHud;
656
+ this._hud.setVisible(!inCockpit && chaseHud);
657
+ this._hud.setHorizonVisible?.(inCockpit);
526
658
  if (showHud) {
527
659
  const RAD = 180 / Math.PI;
528
- this._hud.setMeter('speed', attrs.maxSpeed > 0 ? this.fbw.speed / attrs.maxSpeed : 0);
660
+ this._hud.setMeter('speed', Math.max(attrs.maxSpeed, attrs.afterburnerSpeed) > 0
661
+ ? this.fbw.speed / Math.max(attrs.maxSpeed, attrs.afterburnerSpeed)
662
+ : 0);
529
663
  this._hud.setMeter('altitude', this.altitude / attrs.ceiling);
664
+ // REFERENCE MARKS — what a bare fill can't say.
665
+ //
666
+ // speed: with a throttle lever the needle is always TRAVELLING toward
667
+ // an equilibrium, so mark where it's going; without it, speed rising
668
+ // after you release the trigger reads as a fault rather than as the
669
+ // aircraft doing what you asked (Tonio).
670
+ //
671
+ // altitude: sea level once you're below it, and the GROUND beneath you
672
+ // whenever that's above sea level — the altimeter reads height above
673
+ // datum, which is the wrong number precisely when terrain is the thing
674
+ // about to hit you.
675
+ this._hud.setMeterMarks?.('speed', [
676
+ equilibriumSpeed(cfg, this.fbw.throttle ?? 0, this.fbw.afterburner ?? 0) / topSpeed,
677
+ ]);
678
+ const altMarks = [];
679
+ const groundY = node.position.y - this._lastGroundDist;
680
+ if (groundY > 0)
681
+ altMarks.push(groundY / attrs.ceiling);
682
+ if (this.altitude < 0)
683
+ altMarks.push(0);
684
+ this._hud.setMeterMarks?.('altitude', altMarks);
530
685
  // Nose-up should slide the horizon down — flip here if it reads inverted.
531
686
  this._hud.setHorizon(this.fbw.pitch * RAD, this.fbw.bank * RAD);
532
687
  // Warnings on the graphical HUD: PULL UP flashes the bottom arc (ground
@@ -583,6 +738,10 @@ export class B3dAircraft extends B3dControllable {
583
738
  // wheels — kill the downward bounce and apply rolling resistance so you can
584
739
  // land, roll to a stop, and accelerate to take off again. (First cut — tune
585
740
  // GROUND_FRICTION / GROUND_TOUCH; the model's own ground tweaks are separate.)
741
+ // Inside a cavity (a bore, a cavern) the heightfield rules are suspended —
742
+ // see groundDistance. Computed once per frame, before anything consults it.
743
+ this._inCavity =
744
+ this.owner?.insideCavity(node.position.x, node.position.y, node.position.z) ?? false;
586
745
  const groundDist = this.groundDistance(node); // the ONE raycast this frame
587
746
  this._lastGroundDist = groundDist;
588
747
  const wasGrounded = this.grounded;
@@ -626,6 +785,8 @@ export class B3dAircraft extends B3dControllable {
626
785
  // Read-only flight state (airspeed/altitude/throttle/vtol) is set in the
627
786
  // fly-by-wire block above. Just refresh the ground-proximity pull-up warning.
628
787
  this.altitude = node.position.y;
788
+ this._updateGear(groundDist);
789
+ this._scrubGear(dt);
629
790
  this.updatePullUp(node, groundDist);
630
791
  // Drive the chase pivot LAST — after position is integrated AND ground-clamped, so it uses
631
792
  // THIS frame's final position. (Sampling it earlier, before the move, left the chase a frame
@@ -634,15 +795,60 @@ export class B3dAircraft extends B3dControllable {
634
795
  // level (steady); the Manta bank goes in the camera's own quaternion (a parented FreeCamera
635
796
  // ignores parent-roll and upVector for the view). It re-derives from the airframe each frame,
636
797
  // so a floating-origin rebase is absorbed for free — deliberately NOT origin-registered.
798
+ // LOOK: the right stick swings the view and SPRINGS BACK when released, so
799
+ // you can glance at what you're about to hit without leaving the camera
800
+ // somewhere awkward. Held, it's a slew; released, it returns — which is
801
+ // why it's integrated rather than mapped straight to an angle.
802
+ {
803
+ const attrs2 = this;
804
+ const range = (attrs2.lookRange * Math.PI) / 180;
805
+ const rate = (attrs2.lookRate * Math.PI) / 180;
806
+ const lx = input.lookX ?? 0;
807
+ const ly = input.lookY ?? 0;
808
+ const active = Math.abs(lx) > 0.08 || Math.abs(ly) > 0.08;
809
+ if (active) {
810
+ const yaw = this._lookYaw + lx * rate * dt;
811
+ const pitch = this._lookPitch + ly * rate * dt;
812
+ this._lookYaw = yaw < -range ? -range : yaw > range ? range : yaw;
813
+ const pr = range * 0.5;
814
+ this._lookPitch = pitch < -pr ? -pr : pitch > pr ? pr : pitch;
815
+ }
816
+ else {
817
+ const k = Math.exp(-attrs2.lookReturn * dt); // frame-rate independent
818
+ this._lookYaw *= k;
819
+ this._lookPitch *= k;
820
+ if (Math.abs(this._lookYaw) < 1e-4)
821
+ this._lookYaw = 0;
822
+ if (Math.abs(this._lookPitch) < 1e-4)
823
+ this._lookPitch = 0;
824
+ }
825
+ }
826
+ // COCKPIT: the same look turns the pilot's HEAD — the camera is parented to
827
+ // the airframe, so a local rotation is exactly "look left without turning".
828
+ if (this.cockpitCamera != null) {
829
+ this.cockpitCamera.rotation.set(-this._lookPitch, this._lookYaw, 0);
830
+ }
637
831
  if (this._chasePivot != null) {
638
832
  node.computeWorldMatrix(true); // refresh: position moved since the attitude pass
639
833
  this._chasePivot.position.copyFrom(node.absolutePosition);
640
834
  if (this._chasePivot.rotationQuaternion == null) {
641
835
  this._chasePivot.rotationQuaternion = new BABYLON.Quaternion();
642
836
  }
643
- BABYLON.Quaternion.RotationYawPitchRollToRef(this.fbw.heading, 0, 0, this._chasePivot.rotationQuaternion);
837
+ /*
838
+ ORBIT, not tilt. Both look axes go on the PIVOT, so the camera swings
839
+ around the aircraft on an arc — push up and it rises and looks DOWN at
840
+ the target, which is what "move the camera around the target" means.
841
+
842
+ It keeps aiming at the aircraft for free: the camera sits at a fixed
843
+ local offset behind and above the pivot, so its local aim is constant no
844
+ matter how the pivot is rotated. Putting the pitch on the CAMERA instead
845
+ (what this did first) only tilted the view off the target — the camera
846
+ stayed exactly where it was.
847
+ */
848
+ BABYLON.Quaternion.RotationYawPitchRollToRef(this.fbw.heading + this._lookYaw, this._lookPitch, 0, this._chasePivot.rotationQuaternion);
644
849
  if (this.chaseCamera?.rotationQuaternion != null) {
645
- BABYLON.Quaternion.RotationYawPitchRollToRef(0, this._chaseLookPitch, -this.fbw.bank * CHASE_BANK_FOLLOW, // airframe roll sign convention
850
+ BABYLON.Quaternion.RotationYawPitchRollToRef(0, this._chaseLookPitch, // aim only — the ORBIT lives on the pivot
851
+ -this.fbw.bank * CHASE_BANK_FOLLOW, // airframe roll sign convention
646
852
  this.chaseCamera.rotationQuaternion);
647
853
  }
648
854
  }
@@ -865,10 +1071,119 @@ export class B3dAircraft extends B3dControllable {
865
1071
  }
866
1072
  return best;
867
1073
  }
1074
+ /** The model's gear-retract animations, found by name (see `_findGear`). */
1075
+ _gearGroups = [];
1076
+ /** true = retracted (or retracting). Starts DOWN: you spawn on the ground. */
1077
+ _gearUp = false;
1078
+ _gearSound = null;
1079
+ /** 0 = down, 1 = up — scrubbed toward `_gearTarget`. */
1080
+ _gearPos = 0;
1081
+ _gearTarget = 0;
1082
+ /**
1083
+ * Find the gear animations on a freshly-loaded model, by NAME.
1084
+ *
1085
+ * Convention over configuration: a group whose name mentions "gear" and
1086
+ * "retract" (or "up") is a landing-gear animation, so the scout's
1087
+ * "Main Gear (L) Retract" / "Nose Gear Retract" are picked up with no
1088
+ * authoring beyond what's already in the GLB. Library instances carry their
1089
+ * groups on `metadata.animationGroups` (renamed per instance), so several
1090
+ * aircraft each animate their own gear.
1091
+ */
1092
+ _findGear(node) {
1093
+ const groups = node.metadata?.animationGroups ?? [];
1094
+ this._gearGroups = groups.filter((g) => {
1095
+ const n = g.name.toLowerCase();
1096
+ return n.includes('gear') && (n.includes('retract') || n.includes('up'));
1097
+ });
1098
+ // Started-then-paused so `goToFrame` has an effect: we never let the group
1099
+ // PLAY. See `setGear` for why.
1100
+ for (const g of this._gearGroups) {
1101
+ g.start(false);
1102
+ g.pause();
1103
+ g.goToFrame(g.from); // gear DOWN is frame 0, by the same convention
1104
+ }
1105
+ this._gearPos = 0;
1106
+ this._gearTarget = 0;
1107
+ }
1108
+ /**
1109
+ * Raise or lower the gear. Public so an AI pilot, a cutscene or a key bind
1110
+ * can call it; `autoGear` drives it from altitude otherwise.
1111
+ */
1112
+ setGear(up) {
1113
+ if (up === this._gearUp || this._gearGroups.length === 0)
1114
+ return;
1115
+ this._gearUp = up;
1116
+ this._gearTarget = up ? 1 : 0;
1117
+ this._playGearSound();
1118
+ }
1119
+ /**
1120
+ * The gear is SCRUBBED, not played.
1121
+ *
1122
+ * Letting the AnimationGroup play itself looked obvious and doesn't work:
1123
+ * glTF animations arrive with a cyclic loop mode, so a group told to stop at
1124
+ * the end can snap back to frame 0 — the gear cycles and then vanishes
1125
+ * (exactly what Tonio saw) — and reverse playback via `start(from > to)` is
1126
+ * unreliable across Babylon versions. Advancing a normalised position and
1127
+ * calling `goToFrame` sidesteps all of it: no loop mode, no end-of-group
1128
+ * behaviour, no second animation to author for the reverse, and an
1129
+ * interrupted cycle simply turns around from wherever it had got to.
1130
+ */
1131
+ _scrubGear(dt) {
1132
+ if (this._gearGroups.length === 0)
1133
+ return;
1134
+ const target = this._gearTarget;
1135
+ if (this._gearPos === target)
1136
+ return;
1137
+ const step = dt / Math.max(0.1, this.gearTime);
1138
+ this._gearPos =
1139
+ target > this._gearPos
1140
+ ? Math.min(target, this._gearPos + step)
1141
+ : Math.max(target, this._gearPos - step);
1142
+ for (const g of this._gearGroups) {
1143
+ g.goToFrame(g.from + (g.to - g.from) * this._gearPos);
1144
+ }
1145
+ }
1146
+ /** True while the gear is up (or on its way). */
1147
+ get gearUp() {
1148
+ return this._gearUp;
1149
+ }
1150
+ _playGearSound() {
1151
+ const attrs = this;
1152
+ if (!attrs.gearSound || this.owner == null)
1153
+ return;
1154
+ if (this._gearSound == null) {
1155
+ this._gearSound = new BABYLON.Sound('gear', attrs.gearSound, this.owner.scene, null, { spatialSound: true, volume: attrs.gearVolume, autoplay: false });
1156
+ if (this.meshNode instanceof BABYLON.AbstractMesh) {
1157
+ this._gearSound.attachToMesh(this.meshNode);
1158
+ }
1159
+ }
1160
+ this._gearSound.play();
1161
+ }
1162
+ /** Drive the gear from height above ground, with hysteresis so a bumpy
1163
+ * approach or a hill passing underneath doesn't cycle it. */
1164
+ _updateGear(groundDist) {
1165
+ if (this._gearGroups.length === 0)
1166
+ return;
1167
+ if (isOff(this.autoGear))
1168
+ return;
1169
+ const up = Math.max(1, this.gearAltitude);
1170
+ const down = up * 0.6;
1171
+ const agl = groundDist - this.groundClearance;
1172
+ if (!this._gearUp && agl > up)
1173
+ this.setGear(true);
1174
+ else if (this._gearUp && agl < down)
1175
+ this.setGear(false);
1176
+ }
868
1177
  /** Distance from the aircraft origin down to the nearest ground: the lower of
869
1178
  * any terrain collider the raycast hits and the configured ground plane. */
870
1179
  groundDistance(node) {
871
1180
  const terrain = this.raycastGround(node);
1181
+ // INSIDE a cavity the flat `groundY` floor is a lie: fly into a bore whose
1182
+ // floor is 20m down and the plane term reads −20, so the clamp shoves you
1183
+ // up through the tunnel roof and back into daylight. Underground, only the
1184
+ // ray — the actual surface under you — gets a vote.
1185
+ if (this._inCavity)
1186
+ return terrain;
872
1187
  const plane = node.position.y - (this.groundY ?? 0);
873
1188
  return Math.min(terrain, plane);
874
1189
  }
@@ -922,6 +1237,13 @@ export class B3dAircraft extends B3dControllable {
922
1237
  return Infinity;
923
1238
  }
924
1239
  updatePullUp(node, groundDist) {
1240
+ // No PULL UP inside a tunnel: the ground being 3m below is the POINT of
1241
+ // flying through a bore, and a warning that's always on is a warning
1242
+ // nobody reads when it matters.
1243
+ if (this._inCavity) {
1244
+ this.pullUp = false;
1245
+ return;
1246
+ }
925
1247
  // Warn if projected altitude in PULL_UP_SECONDS is below 10m
926
1248
  const futureY = groundDist < Infinity
927
1249
  ? groundDist + this.velocity.y * PULL_UP_SECONDS
@@ -956,6 +1278,15 @@ export class B3dAircraft extends B3dControllable {
956
1278
  now produce the same identity-frame control node.
957
1279
  */
958
1280
  const control = canonicalize(root, scene, `aircraft-${this.instanceId}`);
1281
+ // Expose the container's AnimationGroups where the library path puts
1282
+ // them, so gear detection (and anything else reading them) works the
1283
+ // same whichever way the model was loaded.
1284
+ if (container.animationGroups.length > 0) {
1285
+ control.metadata = {
1286
+ ...(control.metadata ?? {}),
1287
+ animationGroups: container.animationGroups,
1288
+ };
1289
+ }
959
1290
  this.setupMesh(control, owner);
960
1291
  this.meshesToDispose = [control];
961
1292
  });
@@ -1007,6 +1338,7 @@ export class B3dAircraft extends B3dControllable {
1007
1338
  // With one declared, attitude changes rotate about the CoG while
1008
1339
  // `position` keeps meaning the stance point (parking is unchanged).
1009
1340
  applyCenterOfGravity(root);
1341
+ this._findGear(root);
1010
1342
  if (root instanceof BABYLON.Mesh) {
1011
1343
  this.mesh = root;
1012
1344
  root.ellipsoid = new BABYLON.Vector3(1, 0.5, 2);