Needle Engine

Changes between version 3.22.6 and 3.23.0
Files changed (12) hide show
  1. src/engine-components/Animator.ts +13 -1
  2. src/engine-components/AnimatorController.ts +44 -9
  3. src/engine-components/CharacterController.ts +14 -0
  4. src/engine-components/Collider.ts +6 -0
  5. src/engine/engine_physics_rapier.ts +60 -7
  6. src/engine/engine_serialization_core.ts +23 -14
  7. src/engine/engine_types.ts +15 -2
  8. src/engine/extensions/NEEDLE_animator_controller_model.ts +8 -1
  9. src/engine-components/OrbitControls.ts +18 -12
  10. src/engine-components/timeline/PlayableDirector.ts +10 -3
  11. src/engine-components/timeline/TimelineTracks.ts +1 -1
  12. src/engine-components/webxr/WebARSessionRoot.ts +3 -2
src/engine-components/Animator.ts CHANGED
@@ -41,8 +41,20 @@
41
41
  if (!(val instanceof AnimatorController)) {
42
42
  if (debug) console.log("Assign animator controller", val, this);
43
43
  this._animatorController = new AnimatorController(val);
44
+ if (this.__didAwake)
45
+ this._animatorController.bind(this);
44
46
  }
45
- else this._animatorController = val;
47
+ else {
48
+ if (val.animator && val.animator !== this) {
49
+ console.warn("AnimatorController can not be bound to multiple animators", val.model?.name)
50
+ if (!val.model) {
51
+ console.error("AnimatorController has no model");
52
+ }
53
+ val = new AnimatorController(val.model);
54
+ }
55
+ this._animatorController = val;
56
+ this._animatorController.bind(this);
57
+ }
46
58
  }
47
59
  else this._animatorController = null;
48
60
  }
src/engine-components/AnimatorController.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { deepClone, getParam } from "../engine/engine_utils.js";
6
6
  import { Context } from "../engine/engine_setup.js";
7
7
  import { TypeStore } from "../engine/engine_typestore.js";
8
- import { assign } from "../engine/engine_serialization_core.js";
8
+ import { SerializationContext, TypeSerializer, assign } from "../engine/engine_serialization_core.js";
9
9
  import { Mathf } from "../engine/engine_math.js";
10
10
  import { isAnimationAction } from "../engine/engine_three_utils.js";
11
11
  import { isDevEnvironment } from "../engine/debug/index.js";
@@ -124,7 +124,9 @@
124
124
 
125
125
  setFloat(name: string | number, val: number) {
126
126
  const key = typeof name === "string" ? "name" : "hash";
127
- return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = val);
127
+ const filtered = this.model?.parameters?.filter(p => p[key] === name);
128
+ filtered.forEach(p => p.value = val);
129
+ return filtered?.length > 0;
128
130
  }
129
131
 
130
132
  getFloat(name: string | number): number {
@@ -197,7 +199,8 @@
197
199
  // }
198
200
 
199
201
  bind(animator: Animator) {
200
- if (this.animator !== animator) {
202
+ if (!animator) console.error("AnimatorController.bind: animator is null");
203
+ else if (this.animator !== animator) {
201
204
  this.animator = animator;
202
205
  this._mixer = new AnimationMixer(this.animator.gameObject);
203
206
  this.createActions(this.animator);
@@ -292,9 +295,6 @@
292
295
  }
293
296
 
294
297
  private evaluateTransitions() {
295
-
296
- const currentLayer = 0;
297
-
298
298
  let didEnterStateThisFrame = false;
299
299
  if (!this._activeState) {
300
300
  this.setStartTransition();
@@ -370,6 +370,16 @@
370
370
  // action.time += this.context.time.deltaTime
371
371
  // console.log(action?.time, action?.getEffectiveWeight())
372
372
 
373
+ // update timescale
374
+ if (action) {
375
+ let speedFactor = state.speed ?? 1;
376
+ if (state.speedParameter)
377
+ speedFactor *= this.getFloat(state.speedParameter);
378
+ if (speedFactor !== undefined) {
379
+ action.timeScale = speedFactor * this._speed;
380
+ }
381
+ }
382
+
373
383
  let didTriggerLooping = false;
374
384
  if (state.motion.isLooping && action) {
375
385
  // we dont use the three loop state here because it prevents the transition check above
@@ -472,11 +482,22 @@
472
482
  if (action) {
473
483
 
474
484
  offsetNormalized = Math.max(0, Math.min(1, offsetNormalized));
485
+ if (state.cycleOffsetParameter) {
486
+ const val = this.getFloat(state.cycleOffsetParameter);
487
+ if (typeof val === "number") {
488
+ offsetNormalized += val;
489
+ offsetNormalized %= 1;
490
+ }
491
+ else if (debug) console.warn("AnimatorController cycle offset parameter is not a number", state.cycleOffsetParameter);
492
+ }
493
+ else if (typeof state.cycleOffset === "number") {
494
+ offsetNormalized += state.cycleOffset
495
+ offsetNormalized %= 1;
496
+ }
497
+
475
498
  if (action.isRunning())
476
499
  action.stop();
477
500
  action.reset();
478
- action.timeScale = this._speed;
479
- if (state.speed !== undefined) action.timeScale *= state.speed;
480
501
  action.enabled = true;
481
502
  const duration = state.motion.clip!.duration;
482
503
  action.time = offsetNormalized * duration;
@@ -982,4 +1003,18 @@
982
1003
  }
983
1004
  return null;
984
1005
  }
985
- }
1006
+ }
1007
+
1008
+
1009
+
1010
+ class AnimatorControllerSerializator extends TypeSerializer {
1011
+ onSerialize(_: any, _context: SerializationContext) {
1012
+
1013
+ }
1014
+ onDeserialize(data: AnimatorControllerModel & { __type?: string }, context: SerializationContext) {
1015
+ if (context.type === AnimatorController && data.__type === "AnimatorController")
1016
+ return new AnimatorController(data);
1017
+ return undefined;
1018
+ }
1019
+ }
1020
+ new AnimatorControllerSerializator(AnimatorController);
src/engine-components/CharacterController.ts CHANGED
@@ -85,6 +85,20 @@
85
85
  }
86
86
 
87
87
  get isGrounded(): boolean { return this._activeGroundCollisions.size > 0; }
88
+
89
+ private _contactVelocity: Vector3 = new Vector3();
90
+ get contactVelocity(): Vector3 {
91
+ this._contactVelocity.set(0, 0, 0);
92
+ for (const col of this._activeGroundCollisions) {
93
+ const vel = this.context.physics.engine?.getLinearVelocity(col.collider);
94
+ if (!vel) continue;
95
+ // const friction = col.collider.sharedMaterial?.dynamicFriction || 1;
96
+ this._contactVelocity.x += vel.x;
97
+ this._contactVelocity.y += vel.y;
98
+ this._contactVelocity.z += vel.z;
99
+ }
100
+ return this._contactVelocity;
101
+ }
88
102
  }
89
103
 
90
104
  export class CharacterControllerInput extends Behaviour {
src/engine-components/Collider.ts CHANGED
@@ -53,6 +53,12 @@
53
53
  updateProperties = () => {
54
54
  this.context.physics.engine?.updateProperties(this);
55
55
  }
56
+
57
+ /** Requests an update of the physics material in the physics engine */
58
+ updatePhysicsMaterial(){
59
+ this.context.physics.engine?.updatePhysicsMaterial(this);
60
+
61
+ }
56
62
  }
57
63
 
58
64
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  import { foreachComponent } from './engine_gameobject.js';
19
19
 
20
20
  import { ActiveCollisionTypes, ActiveEvents, CoefficientCombineRule, Ball, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World, Ray, ShapeType, Cuboid } from '@dimforge/rapier3d-compat';
21
- import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
21
+ import { CollisionDetectionMode, PhysicsMaterial, PhysicsMaterialCombine } from '../engine/engine_physics.types.js';
22
22
  import { Gizmos } from './engine_gizmos.js';
23
23
  import { Mathf } from './engine_math.js';
24
24
  import { SphereOverlapResult } from './engine_types.js';
@@ -149,6 +149,8 @@
149
149
  const body = col[$bodyKey];
150
150
  if (body) {
151
151
  this.internalUpdateColliderProperties(col, body);
152
+ if(col.sharedMaterial)
153
+ this.updatePhysicsMaterial(col);
152
154
  }
153
155
  }
154
156
  else {
@@ -169,9 +171,9 @@
169
171
  const body = this.internal_getRigidbody(rigidbody);
170
172
  body?.applyImpulse(force, wakeup)
171
173
  }
172
- getLinearVelocity(rigidbody: IRigidbody): Vec3 | null {
174
+ getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
173
175
  this.validate();
174
- const body = this.internal_getRigidbody(rigidbody);
176
+ const body = this.internal_getRigidbody(comp);
175
177
  if (body) {
176
178
  const vel = body.linvel();
177
179
  return vel;
@@ -557,7 +559,7 @@
557
559
  // Prevent negative scales
558
560
  scale.x = Math.abs(scale.x);
559
561
  scale.y = Math.abs(scale.y);
560
- const desc = ColliderDesc.capsule(height * .5 * scale.y - radius, radius * scale.x);
562
+ const desc = ColliderDesc.capsule(height * .5 * radius * scale.y, radius * scale.x);
561
563
  this.createCollider(collider, desc, center);
562
564
  }
563
565
 
@@ -614,6 +616,55 @@
614
616
  }
615
617
  }
616
618
 
619
+ updatePhysicsMaterial(col: ICollider) {
620
+ if (!col) return;
621
+ const physicsMaterial = col.sharedMaterial;
622
+ const rapier_collider = col[$bodyKey] as Collider;
623
+ if (!rapier_collider) return;
624
+
625
+ if (physicsMaterial) {
626
+ if (physicsMaterial.bounciness !== undefined)
627
+ rapier_collider.setRestitution(physicsMaterial.bounciness);
628
+
629
+ if (physicsMaterial.bounceCombine !== undefined) {
630
+ switch (physicsMaterial.bounceCombine) {
631
+ case PhysicsMaterialCombine.Average:
632
+ rapier_collider.setRestitutionCombineRule(CoefficientCombineRule.Average);
633
+ break;
634
+ case PhysicsMaterialCombine.Maximum:
635
+ rapier_collider.setRestitutionCombineRule(CoefficientCombineRule.Max);
636
+ break;
637
+ case PhysicsMaterialCombine.Minimum:
638
+ rapier_collider.setRestitutionCombineRule(CoefficientCombineRule.Min);
639
+ break;
640
+ case PhysicsMaterialCombine.Multiply:
641
+ rapier_collider.setRestitutionCombineRule(CoefficientCombineRule.Multiply);
642
+ break;
643
+ }
644
+ }
645
+
646
+ if (physicsMaterial.dynamicFriction !== undefined)
647
+ rapier_collider.setFriction(physicsMaterial.dynamicFriction);
648
+
649
+ if (physicsMaterial.frictionCombine !== undefined) {
650
+ switch (physicsMaterial.frictionCombine) {
651
+ case PhysicsMaterialCombine.Average:
652
+ rapier_collider.setFrictionCombineRule(CoefficientCombineRule.Average);
653
+ break;
654
+ case PhysicsMaterialCombine.Maximum:
655
+ rapier_collider.setFrictionCombineRule(CoefficientCombineRule.Max);
656
+ break;
657
+ case PhysicsMaterialCombine.Minimum:
658
+ rapier_collider.setFrictionCombineRule(CoefficientCombineRule.Min);
659
+ break;
660
+ case PhysicsMaterialCombine.Multiply:
661
+ rapier_collider.setFrictionCombineRule(CoefficientCombineRule.Multiply);
662
+ break;
663
+ }
664
+ }
665
+ }
666
+ }
667
+
617
668
  /** Get the rapier body for a Needle component */
618
669
  getBody(obj: ICollider | IRigidbody): null | any {
619
670
  if (!obj) return null;
@@ -651,7 +702,7 @@
651
702
  // TODO: we might want to update this if the material changes
652
703
  const physicsMaterial = collider.sharedMaterial;
653
704
  if (physicsMaterial) {
654
-
705
+
655
706
  if (physicsMaterial.bounciness !== undefined)
656
707
  desc.setRestitution(physicsMaterial.bounciness);
657
708
 
@@ -761,7 +812,8 @@
761
812
  return { rigidBody: rigidBody, useExplicitMassProperties: useExplicitMassProperties };
762
813
  }
763
814
 
764
- private internal_getRigidbody(rb: IRigidbody): RigidBody | null {
815
+ private internal_getRigidbody(rb: IRigidbody | ICollider): RigidBody | null {
816
+ if ((rb as ICollider).isCollider === true) return rb[$colliderRigidbody] as RigidBody;
765
817
  return rb[$bodyKey] as RigidBody;
766
818
  }
767
819
 
@@ -1172,7 +1224,8 @@
1172
1224
  if (pt) {
1173
1225
  const dist = manifold.contactDist(i);
1174
1226
  const friction = manifold.solverContactFriction(i);
1175
- const contact = new ContactPoint(pt, dist, normal, impulse, friction);
1227
+ const tangentVelocity = manifold.solverContactTangentVelocity(i);
1228
+ const contact = new ContactPoint(pt, dist, normal, impulse, friction, tangentVelocity);
1176
1229
  contacts.push(contact);
1177
1230
  if (debugCollisions) {
1178
1231
  Gizmos.DrawDirection(pt, normal, 0xff0000, 3, true);
src/engine/engine_serialization_core.ts CHANGED
@@ -501,6 +501,13 @@
501
501
  // e.g. when @serializable(Texture) and the texture is already resolved via json pointer from gltf
502
502
  // then we dont need to do anything else
503
503
  if (!typeIsFunction && currentValue instanceof type) return currentValue;
504
+
505
+ // try to resolve the serializer for a type only once
506
+ if (!typeContext) {
507
+ typeContext = {
508
+ serializer: helper.getSerializerForConstructor(type)
509
+ }
510
+ }
504
511
 
505
512
  // if the value was already resolved via the persistent asset extension dont try to override that again
506
513
  if (currentValue && typeof currentValue === "object" && isPersistentAsset(currentValue)) {
@@ -517,12 +524,21 @@
517
524
 
518
525
  if (currentValue && type !== undefined) {
519
526
  try {
520
- // we create a concrete instance for a persistent asset here
521
- // hence we want to have the same instance across all usages of this asset
522
- const instance = new type();
523
- if (debugExtension)
524
- console.log("Create concrete instance for persistent asset", currentValue, "instance:", instance);
525
- assign(instance, currentValue);
527
+ let instance = null;
528
+ // It's still possible that the type has an explicit serializer
529
+ // e.g. AnimatorController (or if a user registers a serializer for an arbitrary type and then marks a persistent asset with @serializable)
530
+ // we have this for @serializable(AnimatorController)
531
+ if (typeContext.serializer) {
532
+ instance = typeContext.serializer.onDeserialize(data, context);
533
+ }
534
+ if (!instance) {
535
+ // we create a concrete instance for a persistent asset here
536
+ // hence we want to have the same instance across all usages of this asset
537
+ instance = new type();
538
+ if (debugExtension)
539
+ console.log("Create concrete instance for persistent asset", currentValue, "instance:", instance);
540
+ assign(instance, currentValue);
541
+ }
526
542
  // save it so if another component references the same persistent asset it will automatically use the concrete instance
527
543
  currentValue["__concreteInstance"] = instance;
528
544
  currentValue = instance;
@@ -534,13 +550,6 @@
534
550
  return currentValue;
535
551
  }
536
552
 
537
- // try to resolve the serializer for a type only once
538
- if (!typeContext) {
539
- typeContext = {
540
- serializer: helper.getSerializerForConstructor(type)
541
- }
542
- }
543
-
544
553
  // if the type is an array resolve each entries recursively
545
554
  if (Array.isArray(data)) {
546
555
  const newArr: any[] = [];
@@ -554,7 +563,7 @@
554
563
  return newArr;
555
564
  }
556
565
 
557
- const ser = typeContext.serializer;
566
+ const ser = typeContext?.serializer;
558
567
  if (ser) {
559
568
  return ser.onDeserialize(data, context);
560
569
  }
src/engine/engine_types.ts CHANGED
@@ -244,6 +244,8 @@
244
244
  attachedRigidbody: IRigidbody | null;
245
245
  isTrigger: boolean;
246
246
  sharedMaterial?: PhysicsMaterial;
247
+ updateProperties(): void;
248
+ updatePhysicsMaterial(): void;
247
249
  }
248
250
 
249
251
  export declare interface ISphereCollider extends ICollider {
@@ -309,6 +311,7 @@
309
311
 
310
312
  private readonly _point: Vec3;
311
313
  private readonly _normal: Vec3;
314
+ private readonly _tangentVelocity: Vec3;
312
315
 
313
316
  readonly distance: number;
314
317
  readonly impulse: number;
@@ -326,12 +329,19 @@
326
329
  return target.set(this._normal.x, this._normal.y, this._normal.z);
327
330
  }
328
331
 
329
- constructor(point: Vec3, dist: number, normal: Vec3, impulse: number, friction: number) {
332
+ /** */
333
+ get tangentVelocity() {
334
+ const target = contactsVectorBuffer.get();
335
+ return target.set(this._tangentVelocity.x, this._tangentVelocity.y, this._tangentVelocity.z);
336
+ }
337
+
338
+ constructor(point: Vec3, dist: number, normal: Vec3, impulse: number, friction: number, tangentVelocity: Vec3) {
330
339
  this._point = point;
331
340
  this.distance = dist;
332
341
  this._normal = normal;
333
342
  this.impulse = impulse;
334
343
  this.friction = friction;
344
+ this._tangentVelocity = tangentVelocity;
335
345
  }
336
346
  }
337
347
 
@@ -437,6 +447,8 @@
437
447
  addCapsuleCollider(collider: ICollider, center: Vector3, radius: number, height: number);
438
448
  addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale: Vector3);
439
449
 
450
+ updatePhysicsMaterial(collider:ICollider);
451
+
440
452
  // Rigidbody methods
441
453
  wakeup(rb: IRigidbody);
442
454
  updateProperties(rb: IRigidbody | ICollider);
@@ -444,7 +456,8 @@
444
456
  resetTorques(rb: IRigidbody, wakeup: boolean);
445
457
  addForce(rb: IRigidbody, vec: Vec3, wakeup: boolean);
446
458
  applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean);
447
- getLinearVelocity(rb: IRigidbody): Vec3 | null;
459
+ /** Returns the linear velocity of a rigidbody or the rigidbody of a collider */
460
+ getLinearVelocity(rb: IRigidbody | ICollider): Vec3 | null;
448
461
  getAngularVelocity(rb: IRigidbody): Vec3 | null;
449
462
  setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
450
463
  setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
src/engine/extensions/NEEDLE_animator_controller_model.ts CHANGED
@@ -32,10 +32,17 @@
32
32
  export declare type State = {
33
33
  name: string,
34
34
  hash: number;
35
- speed?: number;
36
35
  motion: Motion,
37
36
  transitions: Transition[],
38
37
  behaviours: StateMachineBehaviourModel[],
38
+ /** The base speed of the animation */
39
+ speed?: number;
40
+ /** Set to a animator controller float parameter name to multiply this ontop of the speed value */
41
+ speedParameter?: string;
42
+ /** Cycle offset normalized 0-1, used when starting a animation */
43
+ cycleOffset?: number;
44
+ /** If set to a parameter then this is used instead of the CycleOffset value to offset the animation start time */
45
+ cycleOffsetParameter?: string;
39
46
  }
40
47
 
41
48
  export declare type StateMachineBehaviourModel = {
src/engine-components/OrbitControls.ts CHANGED
@@ -43,16 +43,16 @@
43
43
  this.controls?.addEventListener("start", callback as any);
44
44
  }
45
45
 
46
+ /** When enabled the scene will be automatically fitted into the camera view in onEnable */
46
47
  @serializable()
48
+ autoFit: boolean = false;
49
+ @serializable()
47
50
  enableRotate: boolean = true;
48
51
  @serializable()
49
52
  autoRotate: boolean = false;
50
53
  @serializable()
51
54
  autoRotateSpeed: number = 1.0;
52
- /** When enabled the scene will be automatically fitted into the camera view in onEnable */
53
55
  @serializable()
54
- autoFit: boolean = false;
55
- @serializable()
56
56
  enableKeys: boolean = true;
57
57
  @serializable()
58
58
  enableDamping: boolean = true;
@@ -70,7 +70,11 @@
70
70
  lookAtConstraint: LookAtConstraint | null = null;
71
71
  @serializable()
72
72
  lookAtConstraint01: number = 1;
73
+
74
+ /** If true user input interrupts the camera from animating to a target */
73
75
  @serializable()
76
+ allowInterrupt: boolean = true;
77
+ @serializable()
74
78
  middleClickToFocus: boolean = true;
75
79
  @serializable()
76
80
  doubleClickToFocus: boolean = true;
@@ -81,6 +85,8 @@
81
85
  useSlerp: boolean = true;
82
86
 
83
87
  debugLog: boolean = false;
88
+
89
+ /** The speed at which the camera target and the camera will be lerping to their destinations (if set via script or user input) */
84
90
  targetLerpSpeed = 5;
85
91
 
86
92
  /** When enabled OrbitControls will automatically raycast find a look at target in start */
@@ -253,10 +259,10 @@
253
259
  if (!this._controls) return;
254
260
  if (this._cameraObject !== this.context.mainCamera) return;
255
261
 
256
- if (this.context.input.getPointerDown(0) || this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2)) {
262
+ if (this.context.input.getPointerDown(1) || this.context.input.getPointerDown(2) || this.context.input.mouseWheelChanged || (this.context.input.getPointerPressed(0) && this.context.input.getPointerPositionDelta(0)?.length() || 0 > .1)) {
257
263
  this._inputs += 1;
258
264
  }
259
- if (this._inputs > 0) {
265
+ if (this._inputs > 0 && this.allowInterrupt) {
260
266
  // if a user has disabled rotation but enabled auto rotate we don't want to change it when we receive input
261
267
  if (this.enableRotate) {
262
268
  this.autoRotate = false;
@@ -271,10 +277,6 @@
271
277
  if (focusAtPointer) {
272
278
  this.setTargetFromRaycast();
273
279
  }
274
- else if (this.context.input.getPointerDown(0) || this.context.input.mouseWheelChanged) {
275
- this._lerpToTargetPosition = false;
276
- this._lerpCameraToTarget = false;
277
- }
278
280
 
279
281
  if (this._lerpToTargetPosition || this._lerpCameraToTarget) {
280
282
  const step = this.context.time.deltaTime * this.targetLerpSpeed;
@@ -293,7 +295,9 @@
293
295
  else {
294
296
  this._cameraObject?.position.lerp(this._cameraTargetPosition, step);
295
297
  }
296
- if (this._cameraObject.position.distanceTo(this._cameraTargetPosition) < .0001) {
298
+ const minDist = this.autoRotate ? .02 : .001;
299
+ const dist = this._cameraObject.position.distanceTo(this._cameraTargetPosition);
300
+ if (dist < minDist) {
297
301
  this._lerpCameraToTarget = false;
298
302
  }
299
303
  }
@@ -307,7 +311,6 @@
307
311
  }
308
312
  }
309
313
 
310
- if (!freeCam && this.lookAtConstraint?.locked) this.setFromTargetPosition(0, this.lookAtConstraint01);
311
314
 
312
315
  if (this._controls) {
313
316
  if (this.debugLog)
@@ -328,9 +331,12 @@
328
331
  }
329
332
  //@ts-ignore
330
333
  // this._controls.zoomToCursor = this.zoomToCursor;
331
- if (!this.context.isInXR)
334
+ if (!this.context.isInXR) {
335
+ if (!freeCam && this.lookAtConstraint?.locked) this.setFromTargetPosition(0, this.lookAtConstraint01);
332
336
  this._controls.update();
337
+ }
333
338
  }
339
+
334
340
  }
335
341
 
336
342
  /** Moves the camera to position smoothly. @deprecated use `setCameraTargetPosition` */
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -183,7 +183,11 @@
183
183
  await delay(200);
184
184
  }
185
185
  this.invokeStateChangedMethodsOnTracks();
186
- this._internalUpdateRoutine = this.startCoroutine(this.internalUpdate(), FrameEvent.OnBeforeRender);
186
+ // Update timeline in LateUpdate to give other scripts time to react to the updated state
187
+ // e.g. if we animate OrbitControls look at target we want those changes to be applied in onBeforeRender
188
+ // if we use onBeforeRender here it will be called *after* the regular onBeforeRender events
189
+ // which is too late
190
+ this._internalUpdateRoutine = this.startCoroutine(this.internalUpdate(), FrameEvent.LateUpdate);
187
191
  }
188
192
 
189
193
  pause() {
@@ -478,7 +482,7 @@
478
482
  this._signalTracks.length = 0;
479
483
 
480
484
  if (!this.playableAsset) return;
481
- const audioListener: AudioListener | null = GameObject.findObjectOfType(AudioListener, this.context);
485
+ let audioListener: AudioListener | null = GameObject.findObjectOfType(AudioListener, this.context);
482
486
  for (const track of this.playableAsset!.tracks) {
483
487
  const type = track.type;
484
488
  const registered = PlayableDirector.createTrackFunctions[type];
@@ -557,7 +561,10 @@
557
561
  audio.director = this;
558
562
  audio.track = track;
559
563
  this._audioTracks.push(audio);
560
- if (!audioListener) continue;
564
+ if (!audioListener) {
565
+ // If the scene doesnt have an AudioListener we add one to the main camera
566
+ audioListener = this.context.mainCameraComponent?.gameObject.addNewComponent(AudioListener)!;
567
+ }
561
568
  audio.listener = audioListener.listener;
562
569
  for (let i = 0; i < track.clips.length; i++) {
563
570
  const clipModel = track.clips[i];
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -648,7 +648,7 @@
648
648
  }
649
649
  if (AudioSource.userInteractionRegistered === false) continue;
650
650
  if (audio === null || !audio.buffer) continue;
651
- audio.playbackRate = this.director.context.time.timeScale;
651
+ audio.playbackRate = this.director.context.time.timeScale * this.director.speed;
652
652
  audio.loop = model.asset.loop;
653
653
  if (time >= model.start && time <= model.end && time < this.director.duration) {
654
654
  if (this.director.isPlaying == false) {
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import { InstancingUtil } from "../../engine/engine_instancing.js";
5
5
  import { serializable } from "../../engine/engine_serialization_decorator.js";
6
6
  import { Context } from "../../engine/engine_context.js";
7
+ import { isQuest } from "../../engine/engine_utils.js";
7
8
 
8
9
  // https://github.com/takahirox/takahirox.github.io/blob/master/js.mmdeditor/examples/js/controls/DeviceOrientationControls.js
9
10
 
@@ -99,8 +100,8 @@
99
100
 
100
101
  if (this.webAR) this.webAR.setReticleActive(false);
101
102
  this.placeAt(rig, poseMatrix);
102
- if (hit && pose)
103
- this.onCreatePlacementAnchor(hit, pose);
103
+ if (hit && pose && !isQuest()) // TODO anchors seem to behave differently with an XRRig
104
+ this.onCreatePlacementAnchor(hit, pose);
104
105
 
105
106
  return true;
106
107
  }