Needle Engine

Changes between version 3.44.5 and 3.44.6
Files changed (6) hide show
  1. src/engine/engine_input.ts +3 -1
  2. src/engine/xr/NeedleXRSession.ts +14 -1
  3. src/engine-components/Voip.ts +29 -5
  4. src/engine-components/webxr/WebXR.ts +1 -1
  5. src/engine-components/webxr/WebXRRig.ts +1 -1
  6. src/engine-components/webxr/controllers/XRControllerMovement.ts +26 -10
src/engine/engine_input.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
4
4
  import { Context } from './engine_setup.js';
5
+ import { getTempVector, getWorldQuaternion } from './engine_three_utils.js';
5
6
  import type { ButtonName, IGameObject, IInput, MouseButtonName, Vec2 } from './engine_types.js';
6
7
  import { type EnumToPrimitiveUnion, getParam, isMozillaXR } from './engine_utils.js';
7
8
 
@@ -958,7 +959,8 @@
958
959
  const pointOnFarPlane = this.tempFarPlaneVector.set(pointOnNearPlane.x, pointOnNearPlane.y, 1);
959
960
  pointOnNearPlane.unproject(camera);
960
961
  pointOnFarPlane.unproject(camera);
961
- this.tempLookMatrix.lookAt(pointOnFarPlane, pointOnNearPlane, (camera as any as IGameObject).worldUp);
962
+ const worldUp = (camera as any as IGameObject).worldUp || getTempVector(0, 1, 0).applyQuaternion(getWorldQuaternion(camera))
963
+ this.tempLookMatrix.lookAt(pointOnFarPlane, pointOnNearPlane, worldUp);
962
964
  space.position.set(pointOnNearPlane.x, pointOnNearPlane.y, pointOnNearPlane.z);
963
965
  space.quaternion.setFromRotationMatrix(this.tempLookMatrix);
964
966
  }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { isDestroyed } from "../engine_gameobject.js";
7
7
  import { Gizmos } from "../engine_gizmos.js";
8
8
  import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
9
- import { getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
9
+ import { getBoundingBox, getTempQuaternion, getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../engine_three_utils.js";
10
10
  import type { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
11
11
  import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
12
12
  import { invokeXRSessionEnd, invokeXRSessionStart } from "./events.js"
@@ -1048,6 +1048,19 @@
1048
1048
  if (!this._didStart) {
1049
1049
  this._didStart = true;
1050
1050
 
1051
+ // place default rig to view the scene
1052
+ if (this.mode === "immersive-vr") {
1053
+ const bounds = getBoundingBox(this.context.scene.children);
1054
+ if (bounds) {
1055
+ const size = bounds.getSize(getTempVector());
1056
+ const rigobject = this._defaultRig.gameObject;
1057
+ rigobject.position.set(bounds.min.x + size.x * .5, bounds.min.y, bounds.max.z + size.z * .5 + 1.5);
1058
+ const centerLook = bounds.getCenter(getTempVector());
1059
+ centerLook.y = rigobject.position.y;
1060
+ rigobject.lookAt(centerLook);
1061
+ }
1062
+ }
1063
+
1051
1064
  invokeXRSessionStart({ session: this });
1052
1065
 
1053
1066
  for (const listener of NeedleXRSession._xrStartListeners) {
src/engine-components/Voip.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { isDevEnvironment, showBalloonError, showBalloonWarning } from "../engine/debug/index.js";
4
4
  import { RoomEvents } from "../engine/engine_networking.js";
5
- import { disposeStream,NetworkedStreamEvents, NetworkedStreams, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js"
5
+ import { disposeStream, NetworkedStreamEvents, NetworkedStreams, StreamEndedEvent, StreamReceivedEvent } from "../engine/engine_networking_streams.js"
6
6
  import { serializable } from "../engine/engine_serialization_decorator.js";
7
7
  import { getParam, microphonePermissionsGranted } from "../engine/engine_utils.js";
8
8
  import { delay } from "../engine/engine_utils.js";
@@ -11,12 +11,23 @@
11
11
  export const noVoip = "noVoip";
12
12
  const debugParam = getParam("debugvoip");
13
13
 
14
+ /**
15
+ * The voice over ip component (Voip) allows you to send and receive audio streams to other users in the same networked room.
16
+ * It requires a networking connection to be working (e.g. by having an active SyncedRoom component in the scene or by connecting to a room manually).
17
+
18
+ */
14
19
  export class Voip extends Behaviour {
15
20
 
16
- /** When enabled VOIP will start when this component becomes enabled */
21
+ /** When enabled VOIP will start when this component becomes enabled
22
+ * @default true
23
+ */
17
24
  @serializable()
18
- autoConnect: boolean = false;
25
+ autoConnect: boolean = true;
19
26
 
27
+ /**
28
+ * When enabled, VOIP will stay connected even when the browser tab is not focused/active anymore
29
+ * @default true
30
+ */
20
31
  @serializable()
21
32
  runInBackground: boolean = true;
22
33
 
@@ -24,6 +35,7 @@
24
35
 
25
36
  private _net!: NetworkedStreams;
26
37
 
38
+ /** @internal */
27
39
  awake() {
28
40
  if (debugParam) this.debug = true;
29
41
  if (this.debug) {
@@ -36,12 +48,11 @@
36
48
  }
37
49
  }
38
50
 
51
+ /** @internal */
39
52
  onEnable(): void {
40
53
  if (!this._net) this._net = NetworkedStreams.create(this);
41
54
  // this._net.debug = this.debug;
42
- //@ts-ignore
43
55
  this._net.addEventListener(NetworkedStreamEvents.StreamReceived, this.onReceiveStream);
44
- //@ts-ignore
45
56
  this._net.addEventListener(NetworkedStreamEvents.StreamEnded, this.onStreamEnded)
46
57
  this._net.enable();
47
58
  if (this.autoConnect) {
@@ -55,6 +66,8 @@
55
66
 
56
67
  window.addEventListener("visibilitychange", this.onVisibilityChanged);
57
68
  }
69
+
70
+ /** @internal */
58
71
  onDisable(): void {
59
72
  this._net.stopSendingStream(this._outputStream);
60
73
  //@ts-ignore
@@ -70,6 +83,9 @@
70
83
 
71
84
  private _outputStream: MediaStream | null = null;
72
85
 
86
+ /**
87
+ * Returns true if the component is currently sending audio
88
+ */
73
89
  get isSending() { return this._outputStream != null && this._outputStream.active; }
74
90
 
75
91
  /** Start sending audio */
@@ -87,6 +103,7 @@
87
103
  disposeStream(this._outputStream);
88
104
  this._outputStream = await this.getAudioStream(audioSource);
89
105
  if (this._outputStream) {
106
+ if (this.debug) console.log("VOIP: Got audio stream");
90
107
  this._net.startSendingStream(this._outputStream);
91
108
  return true;
92
109
  }
@@ -96,6 +113,7 @@
96
113
  }
97
114
  else console.error("VOIP: Could not get audio stream - please make sure to connect an audio device and grant microphone permissions");
98
115
  }
116
+ if (this.debug || isDevEnvironment()) console.log("VOIP: Failed to get audio stream");
99
117
  return false;
100
118
  }
101
119
 
@@ -106,6 +124,9 @@
106
124
  this._outputStream = null;
107
125
  }
108
126
 
127
+ /**
128
+ * Mute or unmute the audio stream
129
+ */
109
130
  setMuted(mute: boolean) {
110
131
  const audio = this._outputStream?.getAudioTracks();
111
132
  if (audio) {
@@ -114,6 +135,8 @@
114
135
  }
115
136
  }
116
137
  }
138
+
139
+ /** Returns true if the audio stream is currently muted */
117
140
  get isMuted() {
118
141
  if (this._outputStream === null) return false;
119
142
  const audio = this._outputStream?.getAudioTracks();
@@ -163,6 +186,7 @@
163
186
 
164
187
  // we have to wait for the user to connect to a room when "auto connect" is enabled
165
188
  private onJoinedRoom = async () => {
189
+ if (this.debug) console.log("VOIP: Joined room");
166
190
  // Wait a moment for user list to be populated
167
191
  await delay(300)
168
192
  if (this.autoConnect && !this.isSending) {
src/engine-components/webxr/WebXR.ts CHANGED
@@ -193,7 +193,7 @@
193
193
  for (const ch of this.context.scene.children)
194
194
  implicitSessionRoot.add(ch);
195
195
  this.context.scene.add(implicitSessionRoot);
196
- sessionroot = GameObject.addNewComponent(implicitSessionRoot, WebARSessionRoot)!;
196
+ sessionroot = GameObject.addComponent(implicitSessionRoot, WebARSessionRoot)!;
197
197
  this._createdComponentsInSession.push(sessionroot);
198
198
  sessionroot.arScale = this.arSceneScale;
199
199
  sessionroot.arTouchTransform = this.usePlacementAdjustment;
src/engine-components/webxr/WebXRRig.ts CHANGED
@@ -52,7 +52,7 @@
52
52
  onEnterXR(args: NeedleXREventArgs): void {
53
53
  this._startScale = this.gameObject.scale.clone();
54
54
  args.xr.addRig(this);
55
- if(debug) console.log("WebXR: add Rig", this.name, this.priority)
55
+ if(debug) console.log("WebXR: add Rig", this.name, this.priority);
56
56
  }
57
57
  onLeaveXR(args: NeedleXREventArgs): void {
58
58
  args.xr.removeRig(this);
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AdditiveBlending, BufferAttribute, Color, DoubleSide, Intersection, Line, Line3, Mesh, MeshBasicMaterial, Object3D, RingGeometry, SphereGeometry, SubtractiveBlending, Vector3 } from "three";
1
+ import { AdditiveBlending, BufferAttribute, Color, DoubleSide, Intersection, Line, Line3, Mesh, MeshBasicMaterial, Object3D, Plane, RingGeometry, SphereGeometry, SubtractiveBlending, Vector3 } from "three";
2
2
  import { Line2 } from "three/examples/jsm/lines/Line2.js";
3
3
  import { LineGeometry } from "three/examples/jsm/lines/LineGeometry.js";
4
4
  import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
@@ -150,28 +150,44 @@
150
150
  else if (teleportInput.y > .8) {
151
151
  this._didTeleport = true;
152
152
  const hit = this.context.physics.raycastFromRay(controller.ray)[0];
153
- if (hit) {
153
+ let point: Vector3 | null = hit?.point;
154
+
155
+ // If we didnt hit an object in the scene use the ground plane
156
+ if (!point) {
157
+ if (!this._plane) {
158
+ this._plane = new Plane(new Vector3(0, 1, 0), 0);
159
+ }
160
+ const currentPosition = rig.worldPosition;
161
+ this._plane.setFromNormalAndCoplanarPoint(new Vector3(0, 1, 0), currentPosition);
162
+ const ray = controller.ray;
163
+ point = currentPosition.clone();
164
+ this._plane.intersectLine(new Line3(ray.origin, getTempVector(ray.direction).multiplyScalar(10000).add(ray.origin)), point);
165
+ if (point.distanceTo(currentPosition) > rig.scale.x * 10) {
166
+ point = null;
167
+ }
168
+ }
169
+
170
+ if (point) {
154
171
  if (this.useTeleportTarget) {
155
172
  const teleportTarget = GameObject.getComponentInParent(hit.object, TeleportTarget);
156
173
  if (!teleportTarget) return;
157
174
  }
158
- if (debug) Gizmos.DrawSphere(hit.point, .025, 0xff0000, 5);
159
- const point = hit.point.clone();
175
+ if (debug) Gizmos.DrawSphere(point, .025, 0xff0000, 5);
176
+ const cloned = point.clone();
160
177
  if (this.useTeleportFade) {
161
178
  controller.xr.fadeTransition()?.then(() => {
162
- rig.worldPosition = point;
179
+ rig.worldPosition = cloned;
163
180
  })
164
181
  }
165
182
  else {
166
- rig.worldPosition = point;
183
+ rig.worldPosition = cloned;
167
184
  }
168
185
  }
169
- else {
170
- // TODO: add option to allow teleportation on current ground plane
171
- }
172
186
  }
173
187
  }
174
188
 
189
+ private _plane: Plane | null = null;
190
+
175
191
  private readonly _lines: Object3D[] = [];
176
192
  private readonly _hitDiscs: Object3D[] = [];
177
193
  private readonly _hitDistances: number[] = [];
@@ -206,7 +222,7 @@
206
222
  line.position.copy(pos);
207
223
  line.quaternion.copy(rot);
208
224
  const scale = session.rigScale;
209
- const dist = this._hitDistances[i] ?? 1;
225
+ const dist = this._hitDistances[i] ?? scale;
210
226
  line.scale.set(scale, scale, dist);
211
227
  line.visible = true;
212
228
  line.layers.disableAll();