@@ -1,9 +1,11 @@
|
|
1
|
-
import { Audio, AudioContext, AudioLoader, PositionalAudio } from "three";
|
1
|
+
import { Audio, AudioContext, AudioLoader, PositionalAudio, Vector3 } from "three";
|
2
2
|
import { PositionalAudioHelper } from 'three/examples/jsm/helpers/PositionalAudioHelper.js';
|
3
3
|
|
4
4
|
import { isDevEnvironment } from "../engine/debug/index.js";
|
5
5
|
import { ApplicationEvents } from "../engine/engine_application.js";
|
6
|
+
import { Mathf } from "../engine/engine_math.js";
|
6
7
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
8
|
+
import { getTempVector } from "../engine/engine_three_utils.js";
|
7
9
|
import * as utils from "../engine/engine_utils.js";
|
8
10
|
import { AudioListener } from "./AudioListener.js";
|
9
11
|
import { Behaviour, GameObject } from "./Component.js";
|
@@ -66,6 +68,9 @@
|
|
66
68
|
playOnAwake: boolean = false;
|
67
69
|
|
68
70
|
@serializable()
|
71
|
+
preload: boolean = false;
|
72
|
+
|
73
|
+
@serializable()
|
69
74
|
get loop(): boolean {
|
70
75
|
if (this.sound) this._loop = this.sound.getLoop();
|
71
76
|
return this._loop;
|
@@ -142,19 +147,62 @@
|
|
142
147
|
if (listener?.listener) {
|
143
148
|
this.sound = new PositionalAudio(listener.listener);
|
144
149
|
this.gameObject?.add(this.sound);
|
150
|
+
|
151
|
+
// this._listener = listener;
|
152
|
+
// this._originalSoundMatrixWorldFunction = this.sound.updateMatrixWorld;
|
153
|
+
// this.sound.updateMatrixWorld = this._onSoundMatrixWorld;
|
145
154
|
}
|
146
155
|
else if (debug) console.warn("No audio listener found in scene - can not play audio");
|
147
156
|
}
|
148
157
|
return this.sound;
|
149
158
|
}
|
150
159
|
|
160
|
+
// This is a hacky workaround to get the PositionalAudio behave like a 2D audio source
|
161
|
+
// private _listener: AudioListener | null = null;
|
162
|
+
// private _originalSoundMatrixWorldFunction: Function | null = null;
|
163
|
+
// private _onSoundMatrixWorld = (force: boolean) => {
|
164
|
+
// if (this._spatialBlend > .05) {
|
165
|
+
// if (this._originalSoundMatrixWorldFunction) {
|
166
|
+
// this._originalSoundMatrixWorldFunction.call(this.sound, force);
|
167
|
+
// }
|
168
|
+
// }
|
169
|
+
// else {
|
170
|
+
// // we use another object's matrix world function (but bound to the positional audio)
|
171
|
+
// // this is just a little trick to prevent calling the PositionalAudio's updateMatrixWorld function
|
172
|
+
// this.gameObject.updateMatrixWorld?.call(this.sound, force);
|
173
|
+
// if (this.sound && this._listener) {
|
174
|
+
// this.sound.gain.connect(this._listener.listener.getInput());
|
175
|
+
// // const pos = getTempVector().setFromMatrixPosition(this._listener.gameObject.matrixWorld);
|
176
|
+
// // const ctx = this.sound.context;
|
177
|
+
// // const delay = this._listener.listener.timeDelta;
|
178
|
+
// // const time = ctx.currentTime ;
|
179
|
+
// // this.sound.panner.positionX.setValueAtTime(pos.x, time);
|
180
|
+
// // this.sound.panner.positionY.setValueAtTime(pos.y, time);
|
181
|
+
// // this.sound.panner.positionZ.setValueAtTime(pos.z, time);
|
182
|
+
// // this.sound.panner.orientationX.setValueAtTime(0, time);
|
183
|
+
// // this.sound.panner.orientationY.setValueAtTime(0, time);
|
184
|
+
// // this.sound.panner.orientationZ.setValueAtTime(-1, time);
|
185
|
+
// }
|
186
|
+
// }
|
187
|
+
// }
|
188
|
+
|
151
189
|
public get ShouldPlay(): boolean { return this.shouldPlay; }
|
152
190
|
|
191
|
+
/** Get the audio context from the Sound */
|
192
|
+
public get audioContext() {
|
193
|
+
return this.sound?.context;
|
194
|
+
}
|
153
195
|
|
154
196
|
awake() {
|
155
|
-
if(debug) console.log(this);
|
197
|
+
if (debug) console.log(this);
|
156
198
|
this.audioLoader = new AudioLoader();
|
157
199
|
if (this.playOnAwake) this.shouldPlay = true;
|
200
|
+
|
201
|
+
if (this.preload) {
|
202
|
+
if (typeof this.clip === "string") {
|
203
|
+
this.audioLoader.load(this.clip, this.createAudio, () => { }, console.error);
|
204
|
+
}
|
205
|
+
}
|
158
206
|
}
|
159
207
|
|
160
208
|
onEnable(): void {
|
@@ -206,50 +254,56 @@
|
|
206
254
|
this.sound?.setVolume(this.volume);
|
207
255
|
}
|
208
256
|
|
209
|
-
private lerp = (x, y, a) => x * (1 - a) + y * a;
|
210
|
-
|
211
257
|
private createAudio = (buffer?: AudioBuffer) => {
|
212
|
-
if (debug) console.log("
|
213
|
-
AudioSource.registerWaitForAllowAudio(() => {
|
214
|
-
if (debug)
|
215
|
-
console.log("finished loading", buffer);
|
258
|
+
if (debug) console.log("AudioBuffer finished loading", buffer);
|
216
259
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
if (sound.isPlaying)
|
223
|
-
sound.stop();
|
260
|
+
const sound = this.Sound;
|
261
|
+
if (!sound) {
|
262
|
+
if (debug) console.warn("Failed getting sound?", this.name);
|
263
|
+
return;
|
264
|
+
}
|
224
265
|
|
225
|
-
|
226
|
-
|
227
|
-
sound.loop = this._loop;
|
228
|
-
if (this.context.application.muted) sound.setVolume(0);
|
229
|
-
else sound.setVolume(this.volume);
|
230
|
-
sound.autoplay = this.shouldPlay;
|
231
|
-
// sound.setDistanceModel('linear');
|
232
|
-
// sound.setRolloffFactor(1);
|
233
|
-
this.applySpatialDistanceSettings();
|
234
|
-
// sound.setDirectionalCone(180, 360, 0.1);
|
235
|
-
if (sound.isPlaying)
|
236
|
-
sound.stop();
|
266
|
+
if (sound.isPlaying)
|
267
|
+
sound.stop();
|
237
268
|
|
238
|
-
|
269
|
+
if (buffer) sound.setBuffer(buffer);
|
270
|
+
sound.loop = this._loop;
|
271
|
+
if (this.context.application.muted) sound.setVolume(0);
|
272
|
+
else sound.setVolume(this.volume);
|
273
|
+
sound.autoplay = this.shouldPlay && AudioSource.userInteractionRegistered;
|
239
274
|
|
240
|
-
|
241
|
-
|
242
|
-
|
275
|
+
this.applySpatialDistanceSettings();
|
276
|
+
|
277
|
+
if (sound.isPlaying)
|
278
|
+
sound.stop();
|
279
|
+
|
280
|
+
// const src = sound.context.createBufferSource();
|
281
|
+
// src.buffer = sound.buffer;
|
282
|
+
// src.connect(sound.panner);
|
283
|
+
// src.start(this.audioContext?.currentTime);
|
284
|
+
// const gain = sound.context.createGain();
|
285
|
+
// gain.gain.value = 1 - this.spatialBlend;
|
286
|
+
// src.connect(gain);
|
287
|
+
|
288
|
+
// make sure we only play the sound if the user has interacted with the page
|
289
|
+
AudioSource.registerWaitForAllowAudio(this.__onAllowAudioCallback);
|
243
290
|
}
|
291
|
+
private __onAllowAudioCallback = () => {
|
292
|
+
if (this.shouldPlay)
|
293
|
+
this.play();
|
294
|
+
}
|
244
295
|
|
245
296
|
private applySpatialDistanceSettings() {
|
246
297
|
const sound = this.sound;
|
247
298
|
if (!sound) return;
|
248
299
|
this._needUpdateSpatialDistanceSettings = false;
|
249
|
-
const dist =
|
300
|
+
const dist = Mathf.lerp(10 * this._maxDistance / Math.max(0.0001, this.spatialBlend), this._minDistance, this.spatialBlend);
|
250
301
|
if (debug) console.log(this.name, this._minDistance, this._maxDistance, this.spatialBlend, "Ref distance=" + dist);
|
251
302
|
sound.setRefDistance(dist);
|
252
303
|
sound.setMaxDistance(Math.max(0.01, this._maxDistance));
|
304
|
+
// sound.setRolloffFactor(this.spatialBlend);
|
305
|
+
// sound.panner.positionZ.automationRate
|
306
|
+
|
253
307
|
// https://developer.mozilla.org/en-US/docs/Web/API/PannerNode/distanceModel
|
254
308
|
switch (this.rollOffMode) {
|
255
309
|
case AudioRolloffMode.Logarithmic:
|
@@ -106,8 +106,14 @@
|
|
106
106
|
onEnable() {
|
107
107
|
super.onEnable();
|
108
108
|
this.context.physics.engine?.addBoxCollider(this, this.size);
|
109
|
+
watchWrite(this.gameObject.scale, this.updateProperties);
|
109
110
|
}
|
110
111
|
|
112
|
+
onDisable(): void {
|
113
|
+
super.onDisable();
|
114
|
+
unwatchWrite(this.gameObject.scale, this.updateProperties);
|
115
|
+
}
|
116
|
+
|
111
117
|
onValidate(): void {
|
112
118
|
this.updateProperties();
|
113
119
|
}
|
@@ -83,8 +83,8 @@
|
|
83
83
|
* @param instance object to instantiate
|
84
84
|
* @param opts options for the instantiation (e.g. with what parent, position, etc.)
|
85
85
|
*/
|
86
|
-
public static instantiate(instance: GameObject | Object3D
|
87
|
-
return instantiate(instance, opts) as GameObject
|
86
|
+
public static instantiate(instance: GameObject | Object3D, opts: IInstantiateOptions | null = null): GameObject {
|
87
|
+
return instantiate(instance, opts) as GameObject;
|
88
88
|
}
|
89
89
|
|
90
90
|
/** Destroys a object on all connected clients (if you are in a networked session)
|
@@ -33,9 +33,11 @@
|
|
33
33
|
idProvider?: UIDProvider;
|
34
34
|
//** parent guid or object */
|
35
35
|
parent?: string | Object3D;
|
36
|
+
/** position in local space. Set `keepWorldPosition` to true if this is world space */
|
36
37
|
position?: Vector3;
|
37
38
|
/** for duplicatable parenting */
|
38
39
|
keepWorldPosition?: boolean;
|
40
|
+
/** rotation in local space. Set `keepWorldPosition` to true if this is world space */
|
39
41
|
rotation?: Quaternion;
|
40
42
|
scale?: Vector3;
|
41
43
|
/** if the instantiated object should be visible */
|
@@ -279,9 +281,7 @@
|
|
279
281
|
clone: Object3D;
|
280
282
|
}
|
281
283
|
|
282
|
-
export function instantiate(instance: GameObject | Object3D
|
283
|
-
if (instance === null) return null;
|
284
|
-
|
284
|
+
export function instantiate(instance: GameObject | Object3D, opts: IInstantiateOptions | null = null): GameObject {
|
285
285
|
let options: InstantiateOptions | null = null;
|
286
286
|
if (opts !== null) {
|
287
287
|
// if x is defined assume this is a vec3 - this is just to not break everything at once and stay a little bit backwards compatible
|
@@ -292,10 +292,6 @@
|
|
292
292
|
else {
|
293
293
|
// if (opts instanceof InstantiateOptions)
|
294
294
|
options = opts as InstantiateOptions;
|
295
|
-
// else {
|
296
|
-
// options = new InstantiateOptions();
|
297
|
-
// Object.assign(options, opts);
|
298
|
-
// }
|
299
295
|
}
|
300
296
|
}
|
301
297
|
|
@@ -898,9 +898,11 @@
|
|
898
898
|
case ShapeType.Cuboid:
|
899
899
|
const cuboid = shape as Cuboid;
|
900
900
|
const sc = col as IBoxCollider;
|
901
|
-
const
|
902
|
-
const
|
903
|
-
const
|
901
|
+
const obj = col.gameObject;
|
902
|
+
const scale = getWorldScale(obj, this._tempPosition);
|
903
|
+
const newX = sc.size.x * 0.5 * scale.x;
|
904
|
+
const newY = sc.size.y * 0.5 * scale.y;
|
905
|
+
const newZ = sc.size.z * 0.5 * scale.z;
|
904
906
|
sizeHasChanged = cuboid.halfExtents.x !== newX || cuboid.halfExtents.y !== newY || cuboid.halfExtents.z !== newZ;
|
905
907
|
cuboid.halfExtents.x = newX;
|
906
908
|
cuboid.halfExtents.y = newY;
|
@@ -189,7 +189,7 @@
|
|
189
189
|
|
190
190
|
/** returns the URL of the default controller model */
|
191
191
|
async getModelUrl(): Promise<string | null> {
|
192
|
-
return this.getMotionController?.then(res => res
|
192
|
+
return this.getMotionController?.then(res => res?.assetUrl || null);
|
193
193
|
}
|
194
194
|
|
195
195
|
constructor(session: NeedleXRSession, device: XRInputSource, index: number) {
|
@@ -362,10 +362,17 @@
|
|
362
362
|
return this.context.physics.engine?.isSleeping(this);
|
363
363
|
}
|
364
364
|
|
365
|
+
/** Call to force an update of the rigidbody properties in the physics engine */
|
366
|
+
public updateProperties() {
|
367
|
+
this._propertiesChanged = false;
|
368
|
+
return this.context.physics.engine?.updateProperties(this);
|
369
|
+
}
|
370
|
+
|
365
371
|
/** Forces affect the rigid-body's acceleration whereas impulses affect the rigid-body's velocity
|
366
372
|
* the acceleration change is equal to the force divided by the mass:
|
367
373
|
* @link see https://rapier.rs/docs/user_guides/javascript/rigid_bodies#forces-and-impulses */
|
368
374
|
public applyForce(vec: Vector3 | Vec3, _rel?: THREE.Vector3, wakeup: boolean = true) {
|
375
|
+
if (this._propertiesChanged) this.updateProperties();
|
369
376
|
this.context.physics.engine?.addForce(this, vec, wakeup);
|
370
377
|
}
|
371
378
|
|
@@ -373,6 +380,7 @@
|
|
373
380
|
* the velocity change is equal to the impulse divided by the mass
|
374
381
|
* @link see https://rapier.rs/docs/user_guides/javascript/rigid_bodies#forces-and-impulses */
|
375
382
|
public applyImpulse(vec: Vector3 | Vec3, wakeup: boolean = true) {
|
383
|
+
if (this._propertiesChanged) this.updateProperties();
|
376
384
|
this.context.physics.engine?.applyImpulse(this, vec, wakeup);
|
377
385
|
}
|
378
386
|
|
@@ -185,11 +185,13 @@
|
|
185
185
|
private _isPlaying: boolean = false;
|
186
186
|
private wasPlaying: boolean = false;
|
187
187
|
|
188
|
-
/** ensure's the video
|
189
|
-
|
188
|
+
/** ensure's the video element has been created and will start loading the clip */
|
189
|
+
preloadVideo() {
|
190
190
|
if (debug) console.log("Video Preload: " + this.name, this.clip);
|
191
191
|
this.create(false);
|
192
192
|
}
|
193
|
+
/** @deprecated use `preloadVideo()` */
|
194
|
+
preload() { this.preloadVideo(); }
|
193
195
|
|
194
196
|
/** Set a new video stream
|
195
197
|
* starts to play automatically if the videoplayer hasnt been active before and playOnAwake is true */
|
@@ -235,7 +237,7 @@
|
|
235
237
|
this.create(true);
|
236
238
|
}
|
237
239
|
else {
|
238
|
-
this.
|
240
|
+
this.preloadVideo();
|
239
241
|
}
|
240
242
|
|
241
243
|
if (this.screenspace) {
|
@@ -307,7 +307,7 @@
|
|
307
307
|
this.imageToObjectMap.set(model, trackedData);
|
308
308
|
|
309
309
|
model.object.loadAssetAsync().then((asset: GameObject | null) => {
|
310
|
-
if (model.createObjectInstance) {
|
310
|
+
if (model.createObjectInstance && asset) {
|
311
311
|
asset = GameObject.instantiate(asset);
|
312
312
|
}
|