@@ -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
|
-
|
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
|
}
|
@@ -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) {
|
@@ -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 =
|
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) {
|
@@ -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.
|
196
|
+
sessionroot = GameObject.addComponent(implicitSessionRoot, WebARSessionRoot)!;
|
197
197
|
this._createdComponentsInSession.push(sessionroot);
|
198
198
|
sessionroot.arScale = this.arSceneScale;
|
199
199
|
sessionroot.arTouchTransform = this.usePlacementAdjustment;
|
@@ -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);
|
@@ -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
|
-
|
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(
|
159
|
-
const
|
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 =
|
179
|
+
rig.worldPosition = cloned;
|
163
180
|
})
|
164
181
|
}
|
165
182
|
else {
|
166
|
-
rig.worldPosition =
|
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] ??
|
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();
|