Needle Engine

Changes between version 3.32.12-alpha and 3.32.13-alpha
Files changed (9) hide show
  1. src/engine-components/AudioSource.ts +86 -32
  2. src/engine-components/Collider.ts +6 -0
  3. src/engine-components/Component.ts +2 -2
  4. src/engine/engine_gameobject.ts +3 -7
  5. src/engine/engine_physics_rapier.ts +5 -3
  6. src/engine/xr/NeedleXRController.ts +1 -1
  7. src/engine-components/RigidBody.ts +8 -0
  8. src/engine-components/VideoPlayer.ts +5 -3
  9. src/engine-components/webxr/WebXRImageTracking.ts +1 -1
src/engine-components/AudioSource.ts CHANGED
@@ -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("audio buffer loaded");
213
- AudioSource.registerWaitForAllowAudio(() => {
214
- if (debug)
215
- console.log("finished loading", buffer);
258
+ if (debug) console.log("AudioBuffer finished loading", buffer);
216
259
 
217
- const sound = this.Sound;
218
- if (!sound) {
219
- console.warn("Failed getting sound", this.name);
220
- return;
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
- if (buffer)
226
- sound.setBuffer(buffer);
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
- if (debug) console.log(this.name, this.shouldPlay, AudioSource.userInteractionRegistered, this);
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
- if (this.shouldPlay && AudioSource.userInteractionRegistered)
241
- this.play();
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 = this.lerp(10 * this._maxDistance / Math.max(0.0001, this.spatialBlend), this._minDistance, this.spatialBlend);
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:
src/engine-components/Collider.ts CHANGED
@@ -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
  }
src/engine-components/Component.ts CHANGED
@@ -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 | null, opts: IInstantiateOptions | null = null): GameObject | null {
87
- return instantiate(instance, opts) as GameObject | null;
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)
src/engine/engine_gameobject.ts CHANGED
@@ -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 | null, opts: IInstantiateOptions | null = null): GameObject | null {
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
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -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 newX = sc.size.x * 0.5;
902
- const newY = sc.size.y * 0.5;
903
- const newZ = sc.size.z * 0.5;
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;
src/engine/xr/NeedleXRController.ts CHANGED
@@ -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.assetUrl || null);
192
+ return this.getMotionController?.then(res => res?.assetUrl || null);
193
193
  }
194
194
 
195
195
  constructor(session: NeedleXRSession, device: XRInputSource, index: number) {
src/engine-components/RigidBody.ts CHANGED
@@ -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
 
src/engine-components/VideoPlayer.ts CHANGED
@@ -185,11 +185,13 @@
185
185
  private _isPlaying: boolean = false;
186
186
  private wasPlaying: boolean = false;
187
187
 
188
- /** ensure's the video elemnent has been created and will start loading the clip */
189
- preload() {
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.preload();
240
+ this.preloadVideo();
239
241
  }
240
242
 
241
243
  if (this.screenspace) {
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -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
  }