Needle Engine

Changes between version 3.32.1-alpha and 3.32.2-alpha
Files changed (5) hide show
  1. src/engine/engine_input.ts +14 -9
  2. src/engine/xr/NeedleXRController.ts +27 -18
  3. src/engine/xr/NeedleXRSession.ts +5 -1
  4. src/engine-components/webxr/WebXRButtons.ts +2 -1
  5. src/engine-components/webxr/controllers/XRControllerMovement.ts +85 -65
src/engine/engine_input.ts CHANGED
@@ -23,15 +23,20 @@
23
23
  readonly source: Event | null;
24
24
 
25
25
  readonly mode: XRTargetRayMode;
26
- /** A ray in worldspace for the event */
26
+ /** A ray in worldspace for the event.
27
+ * If the ray is undefined you can also use `space.worldForward` and `space.worldPosition` */
27
28
  readonly ray?: Ray;
28
- /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix) */
29
+ /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
30
+ * E.g. you can access the input world space source position with `space.worldPosition` or world direction with `space.worldForward`
31
+ */
29
32
  readonly space: IGameObject;
30
33
 
34
+ /** true if this event is a click */
31
35
  isClick: boolean = false;
36
+ /** true if this event is a double click */
32
37
  isDoubleClick: boolean = false;
33
38
 
34
- constructor(type: InputEvents, source: Event | null, init: NEPointerEventInit) {
39
+ constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
35
40
  super(type, init)
36
41
  this.source = source;
37
42
  this.mode = init.mode;
@@ -109,7 +114,7 @@
109
114
  KeyUp = "keyup",
110
115
  KeyPressed = "keypress"
111
116
  }
112
- type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
117
+ export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
113
118
 
114
119
 
115
120
  declare type PointerEventListener = (evt: NEPointerEvent) => void;
@@ -138,7 +143,7 @@
138
143
  if (listeners) {
139
144
  for (const l of listeners) {
140
145
  if (evt.immediatePropagationStopped) {
141
- if(debug) console.log("immediatePropagationStopped", evt.type);
146
+ if (debug) console.log("immediatePropagationStopped", evt.type);
142
147
  break;
143
148
  }
144
149
  l(evt);
@@ -571,7 +576,7 @@
571
576
  private onTouchStartWindow = (evt: TouchEvent) => {
572
577
  // onTouchStart registers on the renderer canvas so that we don't receive events when clicking on UI elements slotted in Needle Engine
573
578
  // however in AR we need to handle these events on the window since they're not emitted otherwise (see NE-4098)
574
- if(!this.context.isInAR) return;
579
+ if (!this.context.isInAR) return;
575
580
  this.onTouchStart(evt);
576
581
  };
577
582
  private onTouchStart = (evt: TouchEvent) => {
@@ -608,7 +613,7 @@
608
613
 
609
614
  if (debug) showBalloonMessage(`touch up #${id}, identifier:${touch.identifier}`);
610
615
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
611
- const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device:space });
616
+ const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
612
617
  this.onUp(ne);
613
618
  }
614
619
  }
@@ -660,7 +665,7 @@
660
665
  private readonly tempLookMatrix = new Matrix4();
661
666
  private getAndUpdateSpatialObjectForScreenPosition(id: number, screenX: number, screenY: number): IGameObject {
662
667
  let space = this._pointerSpace[id]
663
- if (!space) {
668
+ if (!space) {
664
669
  space = new Object3D() as unknown as IGameObject;
665
670
  this._pointerSpace[id] = space;
666
671
  }
@@ -785,7 +790,7 @@
785
790
 
786
791
 
787
792
  if (Math.abs(dx) < 5 && Math.abs(dy) < 5) {
788
- if(debug) console.log("CLICK", index)
793
+ if (debug) console.log("CLICK", index)
789
794
  this.setPointerState(index, this._pointerClick, true);
790
795
  evt.isClick = true;
791
796
 
src/engine/xr/NeedleXRController.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import type { ButtonName, IGameObject, Vec3, XRControllerButtonName } from "../engine_types.js";
4
4
  import { Context } from "../engine_context.js";
5
5
  import { Gizmos } from "../engine_gizmos.js";
6
- import { InputEvents, NEPointerEventInit, PointerType, NEPointerEvent } from "../engine_input.js";
6
+ import { InputEvents, NEPointerEventInit, PointerType, NEPointerEvent, InputEventNames } from "../engine_input.js";
7
7
  import { getTempVector, getTempQuaternion, getWorldQuaternion } from "../engine_three_utils.js";
8
8
  import type { NeedleXRHitTestResult, NeedleXRSession } from "./NeedleXRSession.js";
9
9
  import { flipForwardMatrix, flipForwardQuaternion } from "./internal.js";
@@ -15,7 +15,7 @@
15
15
  declare type ControllerAxes = "xr-standard-thumbstick";
16
16
  declare type StickName = "xr-standard-thumbstick";
17
17
  declare type Mapping = "xr-standard";
18
- declare type ComponentType = "button" | "thumbstick";
18
+ declare type ComponentType = "button" | "thumbstick" | "squeeze";
19
19
  declare type GamepadKey = "button" | "xAxis" | "yAxis";
20
20
 
21
21
 
@@ -68,6 +68,11 @@
68
68
  /** the input source index */
69
69
  readonly index: number = 0;
70
70
 
71
+ /** When enabled the controller will create input events in the Needle Engine input system (e.g. when a button is pressed or the controller is moved)
72
+ * You can disable this if you don't want inputs to go through the input system but be aware that this will result in `onPointerDown` component callbacks to not be invoked anymore for this XRController
73
+ */
74
+ emitEvents = true;
75
+
71
76
  // EXPOSE API
72
77
  /**
73
78
  * Is the controller still connected?
@@ -342,6 +347,7 @@
342
347
  if (componentModel?.gamepadIndices) {
343
348
  switch (componentModel.type) {
344
349
  case "button":
350
+ case "squeeze":
345
351
  if (this.inputSource.gamepad) {
346
352
  const index = componentModel.gamepadIndices!.button!;
347
353
  this._buttonMap.set(key, index);
@@ -508,29 +514,23 @@
508
514
  // and start with index = 1
509
515
  private updateInputEvents() {
510
516
  if (!this._layout) return;
511
- // index 0 is reserved for the primary button
512
- let index = 1;
513
-
517
+
514
518
  // https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-heading
515
519
  if (this.gamepad?.buttons) {
516
520
  for (let k = 0; k < this.gamepad.buttons.length; k++) {
517
- // the selection event is handled in the "selectstart" callback
518
- if (this._selectButtonIndex === k) continue;
519
-
520
521
  const button = this.gamepad.buttons[k];
521
- const i = index++;
522
- const state = this.states[index] || new InputState();
523
- let inputEvent: InputEvents | null = null;
522
+ const state = this.states[k] || new InputState();
523
+ let eventName: InputEventNames | null = null;
524
524
 
525
525
  // is down
526
526
  if (button.pressed && !state.pressed) {
527
- inputEvent = InputEvents.PointerDown;
527
+ eventName = "pointerdown";
528
528
  state.isDown = true;
529
529
  state.isUp = false;
530
530
  }
531
531
  // is up
532
532
  else if (!button.pressed && state.pressed) {
533
- inputEvent = InputEvents.PointerUp;
533
+ eventName = "pointerup"
534
534
  state.isDown = false;
535
535
  state.isUp = true;
536
536
  }
@@ -541,23 +541,32 @@
541
541
 
542
542
  state.value = button.value;
543
543
  state.pressed = button.pressed;
544
- this.states[index] = state;
544
+ this.states[k] = state;
545
545
 
546
- if (inputEvent != null) {
546
+ // the selection event is handled in the "selectstart" callback
547
+ const emitEvent = k !== this._selectButtonIndex && k !== this._squeezeButtonIndex;
548
+
549
+ if (eventName != null && emitEvent) {
547
550
  const name = this._layout?.gamepad[k];
548
- this.emitPointerEvent(inputEvent, i, name ?? "none", false);
551
+ this.emitPointerEvent(eventName, k, name ?? "none", false);
549
552
  }
550
553
  }
551
554
  }
552
555
  }
553
556
  private onUpdateMove() {
554
- this.emitPointerEvent(InputEvents.PointerMove, 0, "none", false);
557
+ this.emitPointerEvent("pointermove", 0, "none", false);
555
558
  }
556
559
 
557
560
 
558
561
  /** cached spatial pointer init object. We re-use it to not have */
559
562
  private readonly pointerInit: NEPointerEventInit;
560
- private emitPointerEvent(type: InputEvents, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null) {
563
+ private emitPointerEvent(type: InputEventNames, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null) {
564
+
565
+ if (!this.emitEvents) {
566
+ if(debug && type !== InputEvents.PointerMove) console.warn("Pointer events are disabled for this controller", this.index, type, button);
567
+ return;
568
+ }
569
+
561
570
  // Currently we do only want to emit pointer events for NON screen based events
562
571
  // that means if the input device is spatial (AR touch on a screen should be handled via touchdown etc still)
563
572
  // Not sure if *this* is enough to determine if the event is spatial or not
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -323,7 +323,10 @@
323
323
 
324
324
  const scripts = mode == "immersive-ar" ? context.scripts_immersive_ar : context.scripts_immersive_vr
325
325
 
326
- console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;", init, scripts);
326
+ if (debug)
327
+ console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;", init, scripts);
328
+ else
329
+ console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;");
327
330
  for (const script of scripts) {
328
331
  if (script.onBeforeXR) script.onBeforeXR(mode, init);
329
332
  }
@@ -347,6 +350,7 @@
347
350
  listener({ mode, init, newSession: newSession || null });
348
351
  }
349
352
  if (!newSession) {
353
+ console.warn("XR Session request was rejected");
350
354
  return null;
351
355
  }
352
356
  return this.setSession(mode, newSession, init, context);
src/engine-components/webxr/WebXRButtons.ts CHANGED
@@ -23,7 +23,8 @@
23
23
  :host {
24
24
  position: absolute;
25
25
  display: flex;
26
- z-index: 100;
26
+ /** increase z-index (nipplejs has 999 as default) */
27
+ z-index: 5000;
27
28
  bottom: 100px;
28
29
  left: 50%;
29
30
  transform: translateX(-50%);
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -26,10 +26,22 @@
26
26
  @serializable()
27
27
  rotationStep = 60;
28
28
 
29
- /** Enable to only oallow teleporting on objects with a teleport target component */
29
+ /** When enabled you can teleport using the right XR controller's thumbstick by pressing forward */
30
30
  @serializable()
31
+ useTeleport: boolean = true;
32
+
33
+ /** Enable to only allow teleporting on objects with a teleport target component */
34
+ @serializable()
31
35
  useTeleportTarget = false;
32
36
 
37
+ /** enable to visualize controller rays in the 3D scene */
38
+ @serializable()
39
+ showRays: boolean = true;
40
+
41
+ /** enable to visualize pointer targets in the 3D scene */
42
+ @serializable()
43
+ showHits: boolean = true;
44
+
33
45
  readonly isXRMovementHandler: true = true;
34
46
 
35
47
  readonly xrSessionMode = "immersive-vr";
@@ -43,8 +55,10 @@
43
55
 
44
56
  // in AR pass through mode we dont want to move the rig
45
57
  if (args.xr.isPassThrough) {
46
- this.renderRays(args.xr);
47
- this.renderHits(args.xr);
58
+ if (this.showRays)
59
+ this.renderRays(args.xr);
60
+ if (this.showHits)
61
+ this.renderHits(args.xr);
48
62
  return;
49
63
  }
50
64
 
@@ -55,11 +69,14 @@
55
69
  this.onHandleMovement(movementController, rig.gameObject);
56
70
  if (teleportController) {
57
71
  this.onHandleRotation(teleportController, rig.gameObject);
58
- this.onHandleTeleport(teleportController, rig.gameObject);
72
+ if (this.useTeleport)
73
+ this.onHandleTeleport(teleportController, rig.gameObject);
59
74
  }
60
75
 
61
- this.renderRays(args.xr);
62
- this.renderHits(args.xr);
76
+ if (this.showRays)
77
+ this.renderRays(args.xr);
78
+ if (this.showHits)
79
+ this.renderHits(args.xr);
63
80
  }
64
81
  onLeaveXR(_: NeedleXREventArgs): void {
65
82
  for (const line of this._lines) {
@@ -149,7 +166,7 @@
149
166
  const ctrl = session.controllers[i];
150
167
  let line = this._lines[i];
151
168
  if (!line) {
152
- line = createGradientLine();
169
+ line = this.createRayLineObject();
153
170
  line.scale.z = .5;
154
171
  this._lines[i] = line;
155
172
  }
@@ -180,7 +197,7 @@
180
197
 
181
198
  let disc = this._hitDiscs[i];
182
199
  if (!disc) {
183
- disc = createHitDisc();
200
+ disc = this.createHitPointObject();
184
201
  this._hitDiscs[i] = disc;
185
202
  }
186
203
  disc.visible = true;
@@ -211,72 +228,75 @@
211
228
  }
212
229
  }
213
230
  }
214
- }
215
231
 
232
+ /** create an object to visualize hit points in the scene */
233
+ protected createHitPointObject(): Object3D {
234
+ var container = new Object3D();
235
+ const disc = new Mesh(
236
+ new RingGeometry(.3, 0.5, 32).rotateX(- Math.PI / 2),
237
+ new MeshBasicMaterial({
238
+ color: 0xeeeeee,
239
+ opacity: .7,
240
+ transparent: true,
241
+ side: DoubleSide,
242
+ })
243
+ );
244
+ disc.layers.disableAll();
245
+ disc.layers.enable(2);
246
+ container.add(disc);
216
247
 
217
- const up = new Vector3(0, 1, 0);
218
- function createHitDisc(): Object3D {
219
- var container = new Object3D();
220
- const disc = new Mesh(
221
- new RingGeometry(.3, 0.5, 32).rotateX(- Math.PI / 2),
222
- new MeshBasicMaterial({
223
- color: 0xeeeeee,
224
- opacity: .7,
225
- transparent: true,
226
- side: DoubleSide,
227
- })
228
- );
229
- disc.layers.disableAll();
230
- disc.layers.enable(2);
231
- container.add(disc);
248
+ const disc2 = new Mesh(
249
+ new RingGeometry(.43, 0.5, 32).rotateX(- Math.PI / 2),
250
+ new MeshBasicMaterial({
251
+ color: 0x000000,
252
+ opacity: .2,
253
+ transparent: true,
254
+ side: DoubleSide,
255
+ })
256
+ );
257
+ disc2.layers.disableAll();
258
+ disc2.layers.enable(2);
259
+ disc2.position.z -= .01;
260
+ container.add(disc2);
261
+ return container;
262
+ }
232
263
 
233
- const disc2 = new Mesh(
234
- new RingGeometry(.43, 0.5, 32).rotateX(- Math.PI / 2),
235
- new MeshBasicMaterial({
236
- color: 0x000000,
237
- opacity: .2,
238
- transparent: true,
239
- side: DoubleSide,
240
- })
241
- );
242
- disc2.layers.disableAll();
243
- disc2.layers.enable(2);
244
- disc2.position.z -= .01;
245
- container.add(disc2);
264
+ /** create an object to visualize controller rays */
265
+ protected createRayLineObject() {
266
+ const line = new Line2();
267
+ line.layers.disableAll();
268
+ line.layers.enable(2);
246
269
 
247
- return container;
248
- }
270
+ const geometry = new LineGeometry();
271
+ line.geometry = geometry;
249
272
 
250
- function createGradientLine() {
251
- const line = new Line2();
252
- line.layers.disableAll();
253
- line.layers.enable(2);
273
+ const positions = new Float32Array(9);
274
+ positions.set([0, 0, .02, 0, 0, .4, 0, 0, 1]);
275
+ geometry.setPositions(positions)
254
276
 
255
- const geometry = new LineGeometry();
256
- line.geometry = geometry;
277
+ const colors = new Float32Array(9);
278
+ colors.set([1, 1, 1, .1, .1, .1, 0, 0, 0]);
279
+ geometry.setColors(colors);
257
280
 
258
- const positions = new Float32Array(9);
259
- positions.set([0, 0, .02, 0, 0, .4, 0, 0, 1]);
260
- geometry.setPositions(positions)
281
+ const mat = new LineMaterial({
282
+ color: 0xffffff,
283
+ vertexColors: true,
284
+ worldUnits: true,
285
+ linewidth: .004,
261
286
 
262
- const colors = new Float32Array(9);
263
- colors.set([1, 1, 1, .1, .1, .1, 0, 0, 0]);
264
- geometry.setColors(colors);
287
+ transparent: true,
288
+ // TODO: this doesnt work with passthrough
289
+ blending: AdditiveBlending,
290
+ dashed: false,
291
+ alphaToCoverage: true,
265
292
 
266
- const mat = new LineMaterial({
267
- color: 0xffffff,
268
- vertexColors: true,
269
- worldUnits: true,
270
- linewidth: .004,
293
+ });
294
+ line.material = mat;
271
295
 
272
- transparent: true,
273
- // TODO: this doesnt work with passthrough
274
- blending: AdditiveBlending,
275
- dashed: false,
276
- alphaToCoverage: true,
296
+ return line;
297
+ }
298
+ }
277
299
 
278
- });
279
- line.material = mat;
280
300
 
281
- return line;
282
- }
301
+ const up = new Vector3(0, 1, 0);
302
+