Needle Engine

Changes between version 3.45.1-beta.7 and 3.45.2-beta
Files changed (6) hide show
  1. src/engine-components/Animation.ts +161 -77
  2. src/engine-components/Component.ts +6 -0
  3. src/engine/engine_context.ts +1 -1
  4. src/engine/engine_input.ts +14 -7
  5. src/engine/xr/NeedleXRSession.ts +1 -1
  6. src/engine-components/Renderer.ts +4 -0
src/engine-components/Animation.ts CHANGED
@@ -9,17 +9,52 @@
9
9
 
10
10
  const debug = getParam("debuganimation");
11
11
 
12
- export declare class PlayOptions {
12
+ export declare type PlayOptions = {
13
+ /**
14
+ * The fade duration in seconds for the action to fade in and other actions to fade out (if exclusive is enabled)
15
+ */
13
16
  fadeDuration?: number;
17
+ /**
18
+ * If true, the animation will loop
19
+ */
14
20
  loop?: boolean;
21
+ /**
22
+ * If true, will stop all other animations before playing this one
23
+ * @default true
24
+ */
15
25
  exclusive?: boolean;
26
+ /**
27
+ * The animation start time in seconds
28
+ */
16
29
  startTime?: number;
30
+ /**
31
+ * The animation end time in seconds
32
+ */
17
33
  endTime?: number;
34
+ /**
35
+ * If true, the animation will clamp when finished
36
+ */
18
37
  clampWhenFinished?: boolean;
38
+ /**
39
+ * Animation playback speed. This is a multiplier to the animation speed
40
+ * @default 1
41
+ */
42
+ speed?: number;
43
+ /**
44
+ * Animation playback speed range. This will override speed
45
+ * @default undefined
46
+ */
19
47
  minMaxSpeed?: Vec2;
48
+ /**
49
+ * The normalized offset to start the animation at. This will override startTime
50
+ * @default undefined
51
+ */
20
52
  minMaxOffsetNormalized?: Vec2;
21
53
  }
22
54
 
55
+
56
+ declare type AnimationIdentifier = AnimationClip | number | string | undefined;
57
+
23
58
  class Vec2 { x!: number; y!: number }
24
59
 
25
60
  /**
@@ -35,6 +70,7 @@
35
70
 
36
71
  @serializable()
37
72
  playAutomatically: boolean = true;
73
+
38
74
  @serializable()
39
75
  randomStartTime: boolean = true;
40
76
 
@@ -46,13 +82,22 @@
46
82
  @serializable()
47
83
  loop: boolean = true;
48
84
 
85
+ /**
86
+ * If true, the animation will clamp when finished
87
+ */
49
88
  @serializable()
50
89
  clampWhenFinished: boolean = false;
51
90
 
52
91
  private _tempAnimationClipBeforeGameObjectExisted: AnimationClip | null = null;
92
+ /**
93
+ * Get the first animation clip in the animations array
94
+ */
53
95
  get clip(): AnimationClip | null {
54
96
  return this.animations?.length ? this.animations[0] : null;
55
97
  }
98
+ /**
99
+ * Set the first animation clip in the animations array
100
+ */
56
101
  set clip(val: AnimationClip | null) {
57
102
  if (!this.__didAwake) {
58
103
  if (debug) console.warn("Assign clip during serialization", val);
@@ -88,20 +133,7 @@
88
133
  get animations(): AnimationClip[] {
89
134
  return this.gameObject.animations || this._tempAnimationsArray || [];
90
135
  }
91
- /**
92
- * @deprecated Currently unsupported
93
- */
94
- get currentAction(): AnimationAction | null {
95
- return this._currentActions[0];
96
- }
97
136
 
98
- /**
99
- * @deprecated Currently unsupported
100
- */
101
- get currentActions(): AnimationAction[] {
102
- return this._currentActions;
103
- }
104
-
105
137
  private mixer: AnimationMixer | undefined = undefined;
106
138
  get actions(): Array<AnimationAction> {
107
139
  return this._actions;
@@ -111,11 +143,9 @@
111
143
  }
112
144
  private _actions: Array<AnimationAction> = [];
113
145
 
114
- // private _currentAction: AnimationAction | null = null;
115
-
116
- private _currentActions: AnimationAction[] = [];
117
146
  private _handles: AnimationHandle[] = [];
118
147
 
148
+ /** @internal */
119
149
  awake() {
120
150
  if (debug) console.log("Animation Awake", this.name, this);
121
151
  if (this._tempAnimationsArray) {
@@ -130,45 +160,43 @@
130
160
  if (this.playAutomatically)
131
161
  this.init();
132
162
  }
133
-
163
+ /** @internal */
134
164
  onEnable(): void {
135
- if (this.playAutomatically && this.animations?.length > 0 && this.currentActions.length <= 0) {
136
- const index = Math.floor(Math.random() * this.actions.length);
137
- this.play(index);
165
+ if (this.playAutomatically && this.animations?.length > 0) {
166
+ const index = Math.floor(Math.random() * this.animations.length);
167
+ const animation = this.animations[index];
168
+ this.play(index, {
169
+ exclusive: true,
170
+ fadeDuration: 0,
171
+ startTime: this.randomStartTime ? Math.random() * animation.duration : 0,
172
+ loop: this.loop,
173
+ clampWhenFinished: this.clampWhenFinished
174
+ });
138
175
  }
139
176
  }
140
-
177
+ /** @internal */
178
+ update() {
179
+ if (!this.mixer) return;
180
+ this.mixer.update(this.context.time.deltaTime);
181
+ }
182
+ /** @internal */
141
183
  onDisable(): void {
142
184
  if (this.mixer) {
143
185
  this.mixer.stopAllAction();
144
186
  this.mixer = undefined;
145
187
  }
146
188
  }
147
-
189
+ /** @internal */
148
190
  onDestroy(): void {
149
191
  this.context.animations.unregisterAnimationMixer(this.mixer);
150
192
  }
151
193
 
152
- start() {
153
- if (this.randomStartTime && this.currentAction)
154
- this.currentAction.time = Math.random() * this.currentAction.getClip().duration;
194
+ /** Get an animation action by the animation clip name */
195
+ getAction(name: string): AnimationAction | null {
196
+ return this.actions?.find(a => a.getClip().name === name) || null;
155
197
  }
156
198
 
157
- update() {
158
- if (!this.mixer) return;
159
- this.mixer.update(this.context.time.deltaTime);
160
- // this is now handled via matrix auto update
161
- // for (const handle of this._handles) {
162
- // handle._update();
163
- // }
164
- // if (this._handles?.length > 0)
165
- // InstancingUtil.markDirty(this.gameObject);
166
- }
167
-
168
- getAction(name: string): AnimationAction | undefined | null {
169
- return this.actions?.find(a => a.getClip().name === name);
170
- }
171
-
199
+ /** Is any animation playing? */
172
200
  get isPlaying() {
173
201
  for (let i = 0; i < this.actions.length; i++) {
174
202
  if (this.actions[i].isRunning())
@@ -177,7 +205,54 @@
177
205
  return false;
178
206
  }
179
207
 
180
- play(clipOrNumber: AnimationClip | number | string | undefined = 0, options?: PlayOptions): Promise<AnimationAction> | void {
208
+ /** Stops all currently playing animations */
209
+ stopAll(opts?: Pick<PlayOptions, "fadeDuration">): void {
210
+ for (const act of this.actions) {
211
+ if (opts?.fadeDuration) {
212
+ act.fadeOut(opts.fadeDuration);
213
+ }
214
+ else {
215
+ act.stop();
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Stops a specific animation clip or index. If clip is undefined then all animations will be stopped
222
+ */
223
+ stop(clip?: AnimationIdentifier, opts?: Pick<PlayOptions, "fadeDuration">): void {
224
+ if (clip === undefined) {
225
+ this.stopAll();
226
+ return;
227
+ }
228
+ else if (typeof clip === "number") {
229
+ if (clip >= this.animations.length) {
230
+ if (debug) console.log("No animation at index", clip)
231
+ return;
232
+ }
233
+ clip = this.animations[clip];
234
+ }
235
+ else if (typeof clip === "string") {
236
+ clip = this.animations.find(a => a.name === clip);
237
+ }
238
+ if (!clip) {
239
+ console.error("Could not find clip", clip)
240
+ return;
241
+ }
242
+ const act = this.actions.find(a => a.getClip() === clip);
243
+ if (!act) {
244
+ console.error("Could not find action", clip)
245
+ return;
246
+ }
247
+ if (opts?.fadeDuration) {
248
+ act.fadeOut(opts.fadeDuration);
249
+ }
250
+ else {
251
+ act.stop();
252
+ }
253
+ }
254
+
255
+ play(clipOrNumber: AnimationIdentifier = 0, options?: PlayOptions): Promise<AnimationAction> | void {
181
256
  if (debug) console.log("PLAY", clipOrNumber)
182
257
  this.init();
183
258
  if (!this.mixer) {
@@ -200,11 +275,9 @@
200
275
  console.error("Could not find clip", clipOrNumber)
201
276
  return;
202
277
  }
278
+
203
279
  if (!options) options = {};
204
- if (!options.minMaxOffsetNormalized) options.minMaxOffsetNormalized = this.minMaxOffsetNormalized;
205
- if (!options.minMaxSpeed) options.minMaxSpeed = this.minMaxSpeed;
206
- if (options.loop === undefined) options.loop = this.loop;
207
- if (options.clampWhenFinished === undefined) options.clampWhenFinished = this.clampWhenFinished;
280
+
208
281
  for (const act of this.actions) {
209
282
  if (act.getClip() === clip) {
210
283
  return this.internalOnPlay(act, options);
@@ -219,45 +292,63 @@
219
292
  return this.internalOnPlay(act, options);
220
293
  }
221
294
 
222
- internalOnPlay(action: AnimationAction, options?: PlayOptions): Promise<AnimationAction> {
223
- var prev = this.currentAction;
295
+ private internalOnPlay(action: AnimationAction, options: PlayOptions): Promise<AnimationAction> {
296
+ var prev = this.actions.find(a => a === action);
224
297
  if (prev === action && prev.isRunning() && prev.time < prev.getClip().duration) {
225
298
  const handle = this.tryFindHandle(action);
226
- if (handle) return handle.getPromise();
299
+ if (handle) return handle.waitForFinish();
227
300
  }
301
+
302
+ // Assign defaults
303
+ if (!options.minMaxOffsetNormalized) options.minMaxOffsetNormalized = this.minMaxOffsetNormalized;
304
+ if (!options.minMaxSpeed) options.minMaxSpeed = this.minMaxSpeed;
305
+ if (options.loop === undefined) options.loop = this.loop;
306
+ if (options.clampWhenFinished === undefined) options.clampWhenFinished = this.clampWhenFinished;
307
+
308
+ // Reset currently running animations
228
309
  const stopOther = options?.exclusive ?? true;
310
+ if (stopOther) {
311
+ this.stopAll(options);
312
+ }
229
313
  if (options?.fadeDuration) {
230
- if (stopOther)
231
- prev?.fadeOut(options.fadeDuration);
232
314
  action.fadeIn(options.fadeDuration);
233
315
  }
316
+ action.enabled = true;
317
+
318
+ // Apply start time
319
+ if (options?.minMaxOffsetNormalized) {
320
+ const clip = action.getClip();
321
+ action.time = Mathf.lerp(options.minMaxOffsetNormalized.x, options.minMaxOffsetNormalized.y, Math.random()) * clip.duration;
322
+ }
323
+ else if (options?.startTime != undefined) {
324
+ action.time = options.startTime;
325
+ }
326
+
327
+ // Apply speed
328
+ if (options?.minMaxSpeed) {
329
+ action.timeScale = Mathf.lerp(options.minMaxSpeed.x, options.minMaxSpeed.y, Math.random());
330
+ }
234
331
  else {
235
- if (stopOther)
236
- prev?.stop();
332
+ action.timeScale = options?.speed ?? 1;
237
333
  }
238
- action.reset();
239
- action.enabled = true;
240
- action.time = 0;
241
- action.timeScale = 1;
242
- const clip = action.getClip();
243
- if (options?.minMaxOffsetNormalized) action.time = Mathf.lerp(options.minMaxOffsetNormalized.x, options.minMaxOffsetNormalized.y, Math.random()) * clip.duration;
244
- if (options?.minMaxSpeed) action.timeScale = Mathf.lerp(options.minMaxSpeed.x, options.minMaxSpeed.y, Math.random());
245
- if (options?.clampWhenFinished) action.clampWhenFinished = true;
246
- if (options?.startTime !== undefined) action.time = options.startTime;
247
334
 
248
- if (options?.loop !== undefined)
335
+ // Apply looping
336
+ if (options?.loop != undefined) {
249
337
  action.loop = options.loop ? LoopRepeat : LoopOnce;
338
+ }
250
339
  else action.loop = LoopOnce;
340
+ if (options?.clampWhenFinished) {
341
+ action.clampWhenFinished = true;
342
+ }
343
+
251
344
  action.play();
252
- if (debug)
253
- console.log("PLAY", action.getClip().name, action)
254
345
 
346
+ if (debug) console.log("PLAY", action.getClip().name, action)
255
347
  const handle = new AnimationHandle(action, this.mixer!, options, _ => {
256
348
  this._handles.splice(this._handles.indexOf(handle), 1);
257
- // console.log(this._handles);
258
349
  });
259
350
  this._handles.push(handle);
260
- return handle.getPromise();
351
+ return handle.waitForFinish();
261
352
  }
262
353
 
263
354
  private tryFindHandle(action: AnimationAction): AnimationHandle | undefined {
@@ -295,44 +386,37 @@
295
386
  private _finishedCallback?: any;
296
387
  private _resolvedOrRejectedCallback?: (AnimationHandle) => void;
297
388
 
298
- constructor(action: AnimationAction, mixer: AnimationMixer, opts?: PlayOptions, cb?: (handle: AnimationHandle) => void) {
389
+ constructor(action: AnimationAction, mixer: AnimationMixer, opts?: PlayOptions, onDone?: (handle: AnimationHandle) => void) {
299
390
  this.action = action;
300
391
  this.mixer = mixer;
301
- this._resolvedOrRejectedCallback = cb;
392
+ this._resolvedOrRejectedCallback = onDone;
302
393
  this._options = opts;
303
394
  }
304
395
 
305
- getPromise(): Promise<AnimationAction> {
396
+ waitForFinish(): Promise<AnimationAction> {
306
397
  if (this.promise) return this.promise;
307
-
308
398
  this.promise = new Promise((res, rej) => {
309
399
  this._resolveCallback = res;
310
400
  this._rejectCallback = rej;
311
401
  this.resolve = this.onResolve.bind(this);
312
402
  this.reject = this.onReject.bind(this);
313
403
  });
314
-
315
404
  this._loopCallback = this.onLoop.bind(this);
316
405
  this._finishedCallback = this.onFinished.bind(this);
317
406
  this.mixer.addEventListener('loop', this._loopCallback);
318
407
  this.mixer.addEventListener('finished', this._finishedCallback);
319
-
320
408
  return this.promise;
321
409
  }
322
410
 
323
411
  _update() {
324
-
325
412
  if (!this._options) return;
326
413
  if (this._options.endTime !== undefined && this.action.time > this._options.endTime) {
327
414
  if (this._options.loop === true) {
328
415
  this.action.time = this._options.startTime ?? 0;
329
416
  }
330
417
  else {
331
- // this.action.stop();
332
418
  this.action.time = this._options.endTime;
333
419
  this.action.timeScale = 0;
334
- // if (!this._options.clampWhenFinished)
335
- // this.action.stop();
336
420
  this.onResolve();
337
421
  }
338
422
  }
src/engine-components/Component.ts CHANGED
@@ -388,6 +388,12 @@
388
388
  get static() {
389
389
  return this.gameObject?.userData.static;
390
390
  }
391
+ set static(value: boolean) {
392
+ if (this.gameObject) {
393
+ if (!this.gameObject.userData) this.gameObject.userData = {}
394
+ this.gameObject.userData.static = value;
395
+ }
396
+ }
391
397
  get hideFlags(): HideFlags {
392
398
  return this.gameObject?.userData.hideFlags;
393
399
  }
src/engine/engine_context.ts CHANGED
@@ -137,7 +137,7 @@
137
137
  */
138
138
  export class Context implements IContext {
139
139
 
140
- private static _defaultTargetFramerate: { value?: number, toString?() } = { value: 60, toString() { return this.value; } }
140
+ private static _defaultTargetFramerate: { value?: number, toString?() } = { value: 90, toString() { return this.value; } }
141
141
  /** When a new context is created this is the framerate that will be used by default */
142
142
  static get DefaultTargetFrameRate(): number | undefined {
143
143
  return Context._defaultTargetFramerate.value;
src/engine/engine_input.ts CHANGED
@@ -854,6 +854,9 @@
854
854
  private onPointerDown = (evt: PointerEvent) => {
855
855
  if (this.context.isInAR) return;
856
856
  if (this.canReceiveInput(evt) === false) return;
857
+ if (evt.target instanceof HTMLElement) {
858
+ evt.target.setPointerCapture(evt.pointerId);
859
+ }
857
860
  const id = this.getPointerId(evt);
858
861
  if (debug) showBalloonMessage(`pointer down #${id}, identifier:${evt.pointerId}`);
859
862
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
@@ -862,7 +865,8 @@
862
865
  }
863
866
  private onPointerMove = (evt: PointerEvent) => {
864
867
  if (this.context.isInAR) return;
865
- if (this.canReceiveInput(evt) === false) return;
868
+ // We want to keep receiving move events until pointerUp and not stop handling events just because we're hovering over *some* HTML element
869
+ // if (this.canReceiveInput(evt) === false) return;
866
870
  let button = evt.button;
867
871
  if (evt.pointerType === "mouse") {
868
872
  const pressedButton = this.getFirstPressedButtonForPointer(0);
@@ -876,8 +880,17 @@
876
880
  const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: evt.pointerType as PointerTypeNames, buttonName: this.getButtonName(evt), device: space, pressure: evt.pressure });
877
881
  this.onMove(ne);
878
882
  }
883
+ private onPointerCancel = (evt: PointerEvent) => {
884
+ if (this.context.isInAR) return;
885
+ if (debug) console.log("Pointer cancel", evt);
886
+ // we treat this as an up event for now to make sure we don't have any pointers stuck in a pressed state etc. Technically we dont want to invoke a up event for cancels...
887
+ this.onPointerUp(evt);
888
+ }
879
889
  private onPointerUp = (evt: PointerEvent) => {
880
890
  if (this.context.isInAR) return;
891
+ if (evt.target instanceof HTMLElement) {
892
+ evt.target.releasePointerCapture(evt.pointerId);
893
+ }
881
894
  // the pointer up event should always be handled
882
895
  // if (this.canReceiveInput(evt) === false) return;
883
896
  const id = this.getPointerId(evt);
@@ -887,12 +900,6 @@
887
900
  this._pointerIds[id] = -1;
888
901
  if (debug) console.log("ID=" + id, "PointerId=" + evt.pointerId, "ALL:", [...this._pointerIds]);
889
902
  }
890
- private onPointerCancel = (evt: PointerEvent) => {
891
- if (this.context.isInAR) return;
892
- if (debug) console.log("Pointer cancel", evt);
893
- // we treat this as an up event for now to make sure we don't have any pointers stuck in a pressed state etc. Technically we dont want to invoke a up event for cancels...
894
- this.onPointerUp(evt);
895
- }
896
903
 
897
904
  private getPointerId(evt: PointerEvent, button?: number): number {
898
905
  if (evt.pointerType === "mouse") return 0 + (button ?? evt.button);
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -842,7 +842,7 @@
842
842
  // handle controller and input source changes changes
843
843
  this.session.addEventListener('end', this.onEnd);
844
844
  // handle input sources change
845
- this.session.addEventListener("inputsourceschange", (evt: XRInputSourceChangeEvent) => {
845
+ this.session.addEventListener("inputsourceschange", (evt: XRInputSourcesChangeEvent) => {
846
846
  // handle removed controllers
847
847
  for (const removedInputSource of evt.removed) {
848
848
  this.disconnectInputSource(removedInputSource);
src/engine-components/Renderer.ts CHANGED
@@ -718,6 +718,10 @@
718
718
  if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
719
719
  this._reflectionProbe.onUnset(this);
720
720
  }
721
+
722
+ if (this.static && this.gameObject.matrixAutoUpdate) {
723
+ this.gameObject.matrixAutoUpdate = false;
724
+ }
721
725
  }
722
726
 
723
727
  /** Applies stencil settings for this renderer's objects (if stencil settings are available) */