Needle Engine

Changes between version 3.32.2-alpha and 3.32.3-alpha
Files changed (9) hide show
  1. src/engine/engine_context.ts +15 -1
  2. src/engine/engine_input.ts +50 -34
  3. src/engine/engine_physics_rapier.ts +12 -6
  4. src/engine/engine_physics.ts +13 -11
  5. src/engine/engine_utils.ts +27 -0
  6. src/engine/xr/NeedleXRController.ts +4 -1
  7. src/engine-components/ParticleSystem.ts +5 -0
  8. src/engine-components/ParticleSystemModules.ts +9 -2
  9. src/engine-components/webxr/WebARSessionRoot.ts +13 -7
src/engine/engine_context.ts CHANGED
@@ -294,11 +294,18 @@
294
294
  readonly scripts_immersive_ar: INeedleXRSessionEventReceiver[] = [];
295
295
  readonly coroutines: { [FrameEvent: number]: Array<CoroutineData> } = {}
296
296
 
297
+ /** callbacks called once after the context has been created */
297
298
  readonly post_setup_callbacks: Function[] = [];
299
+ /** called every frame at the beginning of the frame (after component start events and before earlyUpdate) */
298
300
  readonly pre_update_callbacks: Function[] = [];
301
+ /** called every frame before rendering (after all component events) */
299
302
  readonly pre_render_callbacks: Array<(frame: XRFrame | null) => void> = [];
303
+ /** called every frame after rendering (after all component events) */
300
304
  readonly post_render_callbacks: Function[] = [];
301
305
 
306
+ /** called every frame befroe update (this list is emptied every frame) */
307
+ readonly pre_update_oneshot_callbacks: Function[] = [];
308
+
302
309
  readonly new_scripts: IComponent[] = [];
303
310
  readonly new_script_start: IComponent[] = [];
304
311
  readonly new_scripts_pre_setup_callbacks: Function[] = [];
@@ -410,7 +417,7 @@
410
417
  }
411
418
  }
412
419
  }
413
- if(debug) console.log("Using Renderer Parameters:", params, this.domElement)
420
+ if (debug) console.log("Using Renderer Parameters:", params, this.domElement)
414
421
 
415
422
  this.renderer = new WebGLRenderer(params);
416
423
 
@@ -1078,6 +1085,13 @@
1078
1085
  this.setCurrentCamera(last);
1079
1086
  }
1080
1087
 
1088
+ if (this.pre_update_oneshot_callbacks) {
1089
+ for (const i in this.pre_update_oneshot_callbacks) {
1090
+ this.pre_update_oneshot_callbacks[i]();
1091
+ }
1092
+ this.pre_update_oneshot_callbacks.length = 0;
1093
+ }
1094
+
1081
1095
  if (this.pre_update_callbacks) {
1082
1096
  for (const i in this.pre_update_callbacks) {
1083
1097
  this.pre_update_callbacks[i]();
src/engine/engine_input.ts CHANGED
@@ -6,9 +6,43 @@
6
6
 
7
7
  const debug = getParam("debuginput");
8
8
 
9
+
10
+ export const enum PointerType {
11
+ Mouse = "mouse",
12
+ Touch = "touch",
13
+ Controller = "controller",
14
+ Hand = "hand"
15
+ }
16
+ export type PointerTypeNames = EnumToPrimitiveUnion<PointerType>;
17
+
18
+ const enum PointerEnumType {
19
+ PointerDown = "pointerdown",
20
+ PointerUp = "pointerup",
21
+ PointerMove = "pointermove",
22
+ }
23
+ const enum KeyboardEnumType {
24
+ KeyDown = "keydown",
25
+ KeyUp = "keyup",
26
+ KeyPressed = "keypress"
27
+ }
28
+
29
+ export const enum InputEvents {
30
+ PointerDown = "pointerdown",
31
+ PointerUp = "pointerup",
32
+ PointerMove = "pointermove",
33
+ KeyDown = "keydown",
34
+ KeyUp = "keyup",
35
+ KeyPressed = "keypress"
36
+ }
37
+ export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
38
+
39
+
40
+
9
41
  export declare type NEPointerEventInit = PointerEventInit &
10
42
  {
43
+ origin: object;
11
44
  pointerId: number;
45
+ pointerType: PointerTypeNames;
12
46
  mode: XRTargetRayMode,
13
47
  ray?: Ray;
14
48
  /** The control object for this input. In the case of spatial devices the controller,
@@ -19,6 +53,12 @@
19
53
 
20
54
 
21
55
  export class NEPointerEvent extends PointerEvent {
56
+
57
+ /** The origin of the event contains a reference to the creator of this event.
58
+ * This can be the Needle Engine input system or e.g. a XR controller
59
+ */
60
+ readonly origin: object;
61
+
22
62
  /** the browser event that triggered this event (if any) */
23
63
  readonly source: Event | null;
24
64
 
@@ -36,8 +76,12 @@
36
76
  /** true if this event is a double click */
37
77
  isDoubleClick: boolean = false;
38
78
 
79
+ // this is set via the init arguments (we override it here for intellisense to show the string options)
80
+ override readonly pointerType!: PointerTypeNames;
81
+
39
82
  constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
40
83
  super(type, init)
84
+ this.origin = init.origin;
41
85
  this.source = source;
42
86
  this.mode = init.mode;
43
87
  this.ray = init.ray;
@@ -88,35 +132,7 @@
88
132
  }
89
133
 
90
134
 
91
- export const enum PointerType {
92
- Mouse = "mouse",
93
- Touch = "touch",
94
- Controller = "controller",
95
- Hand = "hand"
96
- }
97
135
 
98
- const enum PointerEnumType {
99
- PointerDown = "pointerdown",
100
- PointerUp = "pointerup",
101
- PointerMove = "pointermove",
102
- }
103
- const enum KeyboardEnumType {
104
- KeyDown = "keydown",
105
- KeyUp = "keyup",
106
- KeyPressed = "keypress"
107
- }
108
-
109
- export const enum InputEvents {
110
- PointerDown = "pointerdown",
111
- PointerUp = "pointerup",
112
- PointerMove = "pointermove",
113
- KeyDown = "keydown",
114
- KeyUp = "keyup",
115
- KeyPressed = "keypress"
116
- }
117
- export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
118
-
119
-
120
136
  declare type PointerEventListener = (evt: NEPointerEvent) => void;
121
137
  declare type KeyboardEventListener = (evt: NEKeyboardEvent) => void;
122
138
  declare type InputEventListener = PointerEventListener | KeyboardEventListener;
@@ -587,7 +603,7 @@
587
603
  const id = this.getPointerIndex(touch.identifier)
588
604
  if (debug) showBalloonMessage(`touch start #${id}, identifier:${touch.identifier}`);
589
605
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
590
- const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
606
+ const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
591
607
  this.onDown(ne);
592
608
  }
593
609
  }
@@ -598,7 +614,7 @@
598
614
  const touch = evt.changedTouches[i];
599
615
  const id = this.getPointerIndex(touch.identifier)
600
616
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
601
- const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
617
+ const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
602
618
  this.onMove(ne);
603
619
  }
604
620
  }
@@ -613,7 +629,7 @@
613
629
 
614
630
  if (debug) showBalloonMessage(`touch up #${id}, identifier:${touch.identifier}`);
615
631
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
616
- const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
632
+ const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
617
633
  this.onUp(ne);
618
634
  }
619
635
  }
@@ -631,7 +647,7 @@
631
647
  case 2: buttonName = "right"; break;
632
648
  }
633
649
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
634
- const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space });
650
+ const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space });
635
651
  this.onDown(ne);
636
652
  }
637
653
 
@@ -640,7 +656,7 @@
640
656
  if (evt.defaultPrevented) return;
641
657
  const id = evt.button;
642
658
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
643
- const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: "none", device: space });
659
+ const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: "none", device: space });
644
660
  this.onMove(ne);
645
661
  }
646
662
 
@@ -656,7 +672,7 @@
656
672
  case 2: buttonName = "right"; break;
657
673
  }
658
674
  const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
659
- const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, });
675
+ const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", pointerId: 0, button: id, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, buttonName: buttonName, device: space, });
660
676
  this.onUp(ne);
661
677
  }
662
678
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -166,12 +166,14 @@
166
166
  addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
167
167
  this.validate();
168
168
  const body = this.internal_getRigidbody(rigidbody);
169
- body?.addForce(force, wakeup)
169
+ if(body) body.addForce(force, wakeup)
170
+ else console.warn("Rigidbody doesn't exist: can not apply force");
170
171
  }
171
172
  addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
172
173
  this.validate();
173
174
  const body = this.internal_getRigidbody(rigidbody);
174
- body?.applyImpulse(force, wakeup)
175
+ if (body) body.applyImpulse(force, wakeup);
176
+ else console.warn("Rigidbody doesn't exist: can not apply impulse");
175
177
  }
176
178
  getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
177
179
  this.validate();
@@ -204,13 +206,15 @@
204
206
  applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
205
207
  this.validate();
206
208
  const body = this.internal_getRigidbody(rb);
207
- body?.applyImpulse(vec, wakeup);
209
+ if(body) body.applyImpulse(vec, wakeup);
210
+ else console.warn("Rigidbody doesn't exist: can not apply impulse");
208
211
  }
209
212
 
210
213
  wakeup(rb: IRigidbody) {
211
214
  this.validate();
212
215
  const body = this.internal_getRigidbody(rb);
213
- body?.wakeUp();
216
+ if(body) body.wakeUp();
217
+ else console.warn("Rigidbody doesn't exist: can not wake up");
214
218
  }
215
219
  isSleeping(rb: IRigidbody) {
216
220
  this.validate();
@@ -220,12 +224,14 @@
220
224
  setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
221
225
  this.validate();
222
226
  const body = this.internal_getRigidbody(rb);
223
- body?.setAngvel(vec, wakeup);
227
+ if(body) body.setAngvel(vec, wakeup);
228
+ else console.warn("Rigidbody doesn't exist: can not set angular velocity");
224
229
  }
225
230
  setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
226
231
  this.validate();
227
232
  const body = this.internal_getRigidbody(rb);
228
- body?.setLinvel(vec, wakeup);
233
+ if(body) body.setLinvel(vec, wakeup);
234
+ else console.warn("Rigidbody doesn't exist: can not set linear velocity");
229
235
  }
230
236
 
231
237
  private context?: IContext;
src/engine/engine_physics.ts CHANGED
@@ -165,17 +165,19 @@
165
165
  if (obj.type === "Mesh" && obj.layers.test(mask) && !Gizmos.isGizmo(obj)) {
166
166
  const mesh = obj as Mesh;
167
167
  const geo = mesh.geometry;
168
- if (!geo.boundingBox)
169
- geo.computeBoundingBox();
170
- if (geo.boundingBox) {
171
- if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
172
- const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
173
- if (sp.intersectsBox(test)) {
174
- const wp = getWorldPosition(obj);
175
- const dist = wp.distanceTo(sp.center);
176
- const int = new SphereIntersection(obj, dist, wp);
177
- results.push(int);
178
- if (!traverseChildsAfterHit) return;
168
+ if (geo) {
169
+ if (!geo.boundingBox)
170
+ geo.computeBoundingBox();
171
+ if (geo.boundingBox) {
172
+ if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
173
+ const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
174
+ if (sp.intersectsBox(test)) {
175
+ const wp = getWorldPosition(obj);
176
+ const dist = wp.distanceTo(sp.center);
177
+ const int = new SphereIntersection(obj, dist, wp);
178
+ results.push(int);
179
+ if (!traverseChildsAfterHit) return;
180
+ }
179
181
  }
180
182
  }
181
183
  }
src/engine/engine_utils.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // use for typesafe interface method calls
2
2
  import { Quaternion, type Vector, Vector2, Vector3, Vector4 } from "three";
3
3
  import { type SourceIdentifier } from "./engine_types.js";
4
+ import { ContextRegistry } from "./engine_context_registry.js";
5
+ import { type Context } from "./engine_context.js";
4
6
 
5
7
  // https://schneidenbach.gitbooks.io/typescript-cookbook/content/nameof-operator.html
6
8
  export const nameofFactory = <T>() => (name: keyof T) => name;
@@ -209,12 +211,37 @@
209
211
  return obj;
210
212
  }
211
213
 
214
+ /** @returns a promise that resolves after a certain amount of milliseconds
215
+ * e.g. `await delay(1000)` will wait for 1 second
216
+ */
212
217
  export function delay(milliseconds: number): Promise<void> {
213
218
  return new Promise((res, _) => {
214
219
  setTimeout(res, milliseconds);
215
220
  });
216
221
  }
217
222
 
223
+ /** @returns a promise that resolves after a certain amount of frames
224
+ * e.g. `await delayForFrames(10)` will wait for 10 frames to pass
225
+ */
226
+ export function delayForFrames(frameCount: number, context?: Context): Promise<void> {
227
+
228
+ if (frameCount <= 0) return Promise.resolve();
229
+ if (!context) context = ContextRegistry.Current as Context;
230
+ if (!context) return Promise.reject("No context");
231
+
232
+ const endFrame = context.time.frameCount + frameCount;
233
+ return new Promise((res, rej) => {
234
+ if (!context) return rej("No context");
235
+ const cb = () => {
236
+ if (context!.time.frameCount >= endFrame) {
237
+ context!.pre_update_callbacks.splice(context!.pre_update_callbacks.indexOf(cb), 1);
238
+ res();
239
+ }
240
+ }
241
+ context!.pre_update_callbacks.push(cb);
242
+ });
243
+ }
244
+
218
245
  // 1) if a timeline is exported via menu item the audio clip path is relative to the glb (same folder)
219
246
  // we need to detect that here and build the new audio source path relative to the new glb location
220
247
  // the same is/might be true for any file that is/will be exported via menu item
src/engine/xr/NeedleXRController.ts CHANGED
@@ -202,6 +202,8 @@
202
202
  this.xr.context.scene.add(this._object);
203
203
  this._ray = new Ray();
204
204
  this.pointerInit = {
205
+ origin: this,
206
+ pointerType: this.hand ? "hand" : "controller",
205
207
  pointerId: -1, // < this will be updated in the emitPointerEvent method
206
208
  mode: this.inputSource.targetRayMode,
207
209
  ray: this._ray,
@@ -571,14 +573,15 @@
571
573
  // that means if the input device is spatial (AR touch on a screen should be handled via touchdown etc still)
572
574
  // Not sure if *this* is enough to determine if the event is spatial or not
573
575
  if (this.xr.mode === "immersive-vr" || this.xr.isPassThrough) {
576
+ this.pointerInit.origin = this;
574
577
  this.pointerInit.pointerId = this.index * 10 + button;
578
+ this.pointerInit.pointerType = this.hand ? "hand" : "controller";
575
579
  this.pointerInit.button = button;
576
580
  this.pointerInit.buttonName = buttonName;
577
581
  this.pointerInit.isPrimary = primary;
578
582
  this.pointerInit.mode = this.inputSource.targetRayMode;
579
583
  this.pointerInit.ray = this.ray;
580
584
  this.pointerInit.device = this.object;
581
- this.pointerInit.pointerType = this.hand ? PointerType.Hand : PointerType.Controller;
582
585
 
583
586
  const prevContext = Context.Current;
584
587
  Context.Current = this.xr.context;
src/engine-components/ParticleSystem.ts CHANGED
@@ -968,6 +968,9 @@
968
968
  const emitter = this._particleSystem.emitter;
969
969
  this.context.scene.add(emitter);
970
970
 
971
+ this.inheritVelocity?.awake();
972
+ this.inheritVelocity.system = this;
973
+
971
974
  if (debug) {
972
975
  console.log(this);
973
976
  this.gameObject.add(new AxesHelper(1))
@@ -1110,6 +1113,8 @@
1110
1113
  this._interface.update();
1111
1114
  this.shape.update(this, this.context, this.main.simulationSpace, this.gameObject);
1112
1115
  this.noise.update(this.context);
1116
+
1117
+ this.inheritVelocity.system = this;
1113
1118
  this.inheritVelocity?.update(this.context);
1114
1119
  this.velocityOverLifetime.update(this);
1115
1120
  }
src/engine-components/ParticleSystemModules.ts CHANGED
@@ -1385,11 +1385,18 @@
1385
1385
  mode!: ParticleSystemInheritVelocityMode;
1386
1386
 
1387
1387
  system!: IParticleSystem;
1388
- private _lastWorldPosition!: Vector3;
1388
+
1389
+ private _lastWorldPosition: Vector3 | null = null;
1389
1390
  private _velocity: Vector3 = new Vector3();
1390
1391
  private _temp: Vector3 = new Vector3();
1391
1392
 
1392
- update(_context: Context) {
1393
+ awake() {
1394
+ this._lastWorldPosition = null!;
1395
+ this._velocity = new Vector3();
1396
+ this._temp = new Vector3();
1397
+ }
1398
+
1399
+ update (_context: Context) {
1393
1400
  if (!this.enabled) return;
1394
1401
  if (this.system.worldspace === false) return;
1395
1402
  if (this._lastWorldPosition) {
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { serializable } from "../../engine/engine_serialization_decorator.js";
4
4
  import { Context } from "../../engine/engine_context.js";
5
5
  import { IComponent, IGameObject } from "../../engine/engine_types.js";
6
- import { NeedleXREventArgs, NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
6
+ import { NeedleXRController, NeedleXREventArgs, NeedleXRHitTestResult, NeedleXRSession } from "../../engine/engine_xr.js";
7
7
  import { NEPointerEvent } from "../../engine/engine_input.js";
8
8
  import { getParam } from "../../engine/engine_utils.js";
9
9
  import { destroy } from "../../engine/engine_gameobject.js";
@@ -122,6 +122,7 @@
122
122
  for (const ret of this._reticle) {
123
123
  destroy(ret);
124
124
  }
125
+ this._reticle.length = 0;
125
126
  this._isPlacing = true;
126
127
  this.context.input.addEventListener("pointerup", this.onPlaceScene);
127
128
  }
@@ -148,7 +149,9 @@
148
149
  if (rigObject && rigObject.parent !== this.context.scene) {
149
150
  this.context.scene.add(rigObject);
150
151
  }
152
+
151
153
  // in pass through mode we want to place the scene using an XR controller
154
+ let controllersDidHit = false;
152
155
  if (args.xr.isPassThrough && args.xr.controllers.length > 0) {
153
156
  for (const ctrl of args.xr.controllers) {
154
157
  // with this we can only place with the left / first controller right now
@@ -156,12 +159,13 @@
156
159
  // and then place at the reticle for which the user clicked the place button
157
160
  const hit = ctrl.getHitTest();
158
161
  if (hit) {
162
+ controllersDidHit = true;
159
163
  this.updateReticleAndHits(args.xr, ctrl.index, hit, args.xr.rigScale);
160
164
  }
161
165
  }
162
166
  }
163
- // in screen AR mode we use "camera" hit testing
164
- else {
167
+ // in screen AR mode we use "camera" hit testing (or when using the simulator where controller hit testing is not supported)
168
+ if (!controllersDidHit) {
165
169
  const hit = args.xr.getHitTest();
166
170
  if (hit) {
167
171
  this.updateReticleAndHits(args.xr, 0, hit, args.xr.rigScale);
@@ -246,10 +250,10 @@
246
250
  let reticle = this._reticle[0];
247
251
  let hit = this._hits[0];
248
252
 
249
- if (evt.mode === "tracked-pointer") {
253
+ if (evt.origin instanceof NeedleXRController) {
250
254
  // until we can use hit testing for both controllers and have multple reticles we only allow placement with the first controller
251
- reticle = this._reticle[evt.pointerId];
252
- hit = this._hits[evt.pointerId];
255
+ reticle = this._reticle[evt.origin.index];
256
+ hit = this._hits[evt.origin.index];
253
257
  }
254
258
 
255
259
  if (!reticle) {
@@ -346,7 +350,9 @@
346
350
  rigObject.position.multiplyScalar(this.arScale);
347
351
 
348
352
  rigObject.updateMatrix();
349
- if (this.invertForward)
353
+ // if invert forward is disabled we need to invert the forward rotation
354
+ // we want to look into positive Z direction (if invertForward is enabled we look into negative Z direction)
355
+ if (!this.invertForward)
350
356
  rigObject.matrix.premultiply(invertForwardMatrix);
351
357
  rigObject.matrix.premultiply(this._startOffset);