Needle Engine

Changes between version 3.19.0 and 3.19.1
Files changed (9) hide show
  1. src/engine-components/ui/Canvas.ts +25 -6
  2. src/engine-components/CharacterController.ts +13 -9
  3. src/engine-components/ui/EventSystem.ts +7 -3
  4. src/engine-components/ui/Interfaces.ts +7 -0
  5. src/engine-components/ParticleSystem.ts +19 -14
  6. src/engine-components/SceneSwitcher.ts +9 -1
  7. src/engine-components/ui/Text.ts +16 -7
  8. src/engine-components/webxr/WebARSessionRoot.ts +35 -5
  9. src/engine-components/webxr/WebXR.ts +3 -2
src/engine-components/ui/Canvas.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { GameObject } from "../Component.js";
6
6
  import { Matrix4, Object3D } from "three";
7
7
  import { RectTransform } from "./RectTransform.js";
8
- import { ICanvas, ILayoutGroup, IRectTransform } from "./Interfaces.js";
8
+ import { ICanvas, ICanvasEventReceiver, ILayoutGroup, IRectTransform } from "./Interfaces.js";
9
9
  import { Camera } from "../Camera.js";
10
10
  import { EventSystem } from "./EventSystem.js";
11
11
  import * as ThreeMeshUI from 'three-mesh-ui'
@@ -189,15 +189,26 @@
189
189
  this._layoutGroups.delete(obj);
190
190
  }
191
191
 
192
+ private _receivers: ICanvasEventReceiver[] = [];
193
+ registerEventReceiver(receiver: ICanvasEventReceiver) {
194
+ this._receivers.push(receiver);
195
+ }
196
+ unregisterEventReceiver(receiver: ICanvasEventReceiver) {
197
+ const index = this._receivers.indexOf(receiver);
198
+ if (index !== -1) {
199
+ this._receivers.splice(index, 1);
200
+ }
201
+ }
202
+
192
203
  onBeforeRenderRoutine = () => {
193
-
194
204
  if (this.context.isInVR) {
195
205
  this.onUpdateRenderMode();
196
206
  this.handleLayoutUpdates();
197
207
  // TODO TMUI @swingingtom - For VR this is so we don't have text clipping
198
208
  this.shadowComponent?.updateMatrixWorld(true);
199
209
  this.shadowComponent?.updateWorldMatrix(true, true);
200
- EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
210
+ this.invokeBeforeRenderEvents();
211
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
201
212
  return;
202
213
  }
203
214
 
@@ -214,6 +225,7 @@
214
225
  // TODO: we might need to optimize this. This is here to make sure the TMUI text clipping matrices are correct. Ideally the text does use onBeforeRender and apply the clipping matrix there so we dont have to force update all the matrices here
215
226
  this.shadowComponent?.updateMatrixWorld(true);
216
227
  this.shadowComponent?.updateWorldMatrix(true, true);
228
+ this.invokeBeforeRenderEvents();
217
229
  EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
218
230
  }
219
231
  }
@@ -236,7 +248,8 @@
236
248
  this.handleLayoutUpdates();
237
249
  this.shadowComponent?.updateMatrixWorld(true);
238
250
  // this.handleLayoutUpdates();
239
- EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
251
+ this.invokeBeforeRenderEvents();
252
+ EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
240
253
  this.context.renderer.render(this.gameObject, this.context.mainCamera);
241
254
  this.context.renderer.autoClear = prevAutoClearDepth;
242
255
  this.context.renderer.autoClearColor = prevAutoClearColor;
@@ -245,6 +258,12 @@
245
258
  this._lastMatrixWorld?.copy(this.gameObject.matrixWorld);
246
259
  }
247
260
 
261
+ private invokeBeforeRenderEvents() {
262
+ for (const receiver of this._receivers) {
263
+ receiver.onBeforeCanvasRender?.(this);
264
+ }
265
+ }
266
+
248
267
  private handleLayoutUpdates() {
249
268
  if (this._lastMatrixWorld === null) {
250
269
  this._lastMatrixWorld = new Matrix4();
@@ -309,8 +328,8 @@
309
328
  let camera = this.context.mainCameraComponent;
310
329
  let planeDistance: number = 10;
311
330
  if (camera && camera.nearClipPlane > 0 && camera.farClipPlane > 0) {
312
- // TODO: this is a hack/workaround for event system currently only passing events to the nearest object
313
- planeDistance = Mathf.lerp(camera.nearClipPlane, camera.farClipPlane, .15);
331
+ // TODO: this is a hack/workaround for event system currently only passing events to the nearest object so we move the canvas close to the nearplane
332
+ planeDistance = Mathf.lerp(camera.nearClipPlane, camera.farClipPlane, .01);
314
333
  }
315
334
  if (this._renderMode === RenderMode.ScreenSpaceCamera) {
316
335
  if (this.worldCamera)
src/engine-components/CharacterController.ts CHANGED
@@ -38,21 +38,25 @@
38
38
  let collider = this.gameObject.getComponent(CapsuleCollider);
39
39
  if (!collider)
40
40
  collider = this.gameObject.addNewComponent(CapsuleCollider) as CapsuleCollider;
41
- // rb.isKinematic = true;
41
+
42
42
  collider.center.copy(this.center);
43
43
  collider.radius = this.radius;
44
44
  collider.height = this.height;
45
- this.gameObject.rotation.x = 0;
46
- this.gameObject.rotation.z = 0;
45
+
46
+ // discard any rotation besides Y axis
47
+ const wForward = new Vector3(0, 0, 1);
48
+ const wRight = new Vector3(1, 0, 0);
49
+ const wUp = new Vector3(0, 1, 0);
50
+ const fwd = this.gameObject.getWorldDirection(new Vector3());
51
+ fwd.y = 0;
52
+
53
+ const sign = wRight.dot(fwd) < 0 ? -1 : 1;
54
+ const angleY = wForward.angleTo(fwd) * sign;
55
+ this.gameObject.setRotationFromAxisAngle(wUp, angleY);
56
+
47
57
  rb.lockRotationX = true;
48
58
  rb.lockRotationY = true;
49
59
  rb.lockRotationZ = true;
50
-
51
- // TODO: this doesnt work yet
52
- // setInterval(()=>{
53
- // this.rigidbody.isKinematic = !this.rigidbody.isKinematic;
54
- // console.log(this.rigidbody.isKinematic);
55
- // }, 1000)
56
60
  }
57
61
 
58
62
  move(vec: Vector3) {
src/engine-components/ui/EventSystem.ts CHANGED
@@ -68,8 +68,8 @@
68
68
  }
69
69
 
70
70
  //@ts-ignore
71
- static ensureUpdateMeshUI(instance, context: Context) {
72
- MeshUIHelper.update(instance, context);
71
+ static ensureUpdateMeshUI(instance, context: Context, force: boolean = false) {
72
+ MeshUIHelper.update(instance, context, force);
73
73
  }
74
74
  static markUIDirty(_context: Context) {
75
75
  MeshUIHelper.markDirty();
@@ -533,7 +533,11 @@
533
533
  this.needsUpdate = true;
534
534
  }
535
535
 
536
- static update(threeMeshUI: any, context: Context) {
536
+ static update(threeMeshUI: any, context: Context, force: boolean = false) {
537
+ if (force) {
538
+ threeMeshUI.update();
539
+ return;
540
+ }
537
541
  const currentFrame = context.time.frameCount;
538
542
  for (const lu of this.lastUpdateFrame) {
539
543
  if (lu.context === context) {
src/engine-components/ui/Interfaces.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  get screenspace(): boolean;
7
7
  registerTransform(rt: IRectTransform);
8
8
  unregisterTransform(rt: IRectTransform);
9
+ registerEventReceiver(receiver: ICanvasEventReceiver);
10
+ unregisterEventReceiver(receiver: ICanvasEventReceiver);
9
11
  }
10
12
 
11
13
  export interface ICanvasGroup {
@@ -39,6 +41,11 @@
39
41
  updateLayout();
40
42
  }
41
43
 
44
+ export interface ICanvasEventReceiver {
45
+ /** Called before the canvas is rendering */
46
+ onBeforeCanvasRender?(canvas: ICanvas);
47
+ }
48
+
42
49
  // export abstract class LayoutGroup extends Behaviour implements IRectTransformChangedReceiver, ILayoutGroup {
43
50
  // get isLayoutGroup(): boolean {
44
51
  // return true;
src/engine-components/ParticleSystem.ts CHANGED
@@ -52,6 +52,12 @@
52
52
  @serializable()
53
53
  minParticleSize!: number;
54
54
 
55
+ @serializable()
56
+ velocityScale?: number;
57
+ @serializable()
58
+ cameraVelocityScale?: number;
59
+ @serializable()
60
+ lengthScale?: number;
55
61
 
56
62
  start() {
57
63
  if (this.maxParticleSize !== .5 && this.minParticleSize !== 0) {
@@ -99,15 +105,8 @@
99
105
  return material;
100
106
  }
101
107
 
102
- getMesh(renderMode?: ParticleSystemRenderMode) {
108
+ getMesh(_renderMode?: ParticleSystemRenderMode) {
103
109
  let geo: BufferGeometry | null = null;
104
- if (renderMode === ParticleSystemRenderMode.HorizontalBillboard) {
105
- geo = new THREE.BoxGeometry(1, 1, 0);
106
- }
107
- else if (renderMode === ParticleSystemRenderMode.VerticalBillboard) {
108
- geo = new THREE.BoxGeometry(1, 0, 1);
109
- }
110
-
111
110
  if (!geo) {
112
111
  if (this.particleMesh instanceof Mesh) {
113
112
  geo = this.particleMesh.geometry;
@@ -556,7 +555,7 @@
556
555
  }
557
556
 
558
557
  get prewarm() { return false; } // force disable three.quark prewarm, we have our own!
559
- get material() { return this.system.renderer.getMaterial(this.system.trails.enabled) as Material;}
558
+ get material() { return this.system.renderer.getMaterial(this.system.trails.enabled) as Material; }
560
559
  get layers() { return this.system.gameObject.layers; }
561
560
 
562
561
  update() {
@@ -590,8 +589,8 @@
590
589
  switch (this.system.renderer.renderMode) {
591
590
  case ParticleSystemRenderMode.Billboard: return RenderMode.BillBoard;
592
591
  case ParticleSystemRenderMode.Stretch: return RenderMode.StretchedBillBoard;
593
- case ParticleSystemRenderMode.HorizontalBillboard: return RenderMode.BillBoard;
594
- case ParticleSystemRenderMode.VerticalBillboard: return RenderMode.BillBoard;
592
+ case ParticleSystemRenderMode.HorizontalBillboard: return RenderMode.HorizontalBillBoard;
593
+ case ParticleSystemRenderMode.VerticalBillboard: return RenderMode.VerticalBillBoard;
595
594
  case ParticleSystemRenderMode.Mesh: return RenderMode.Mesh;
596
595
  }
597
596
  return RenderMode.BillBoard;
@@ -600,7 +599,13 @@
600
599
  startLength: new ConstantValue(220),
601
600
  followLocalOrigin: false,
602
601
  };
603
- get speedFactor() { return this.system.main.simulationSpeed; }
602
+ get speedFactor() {
603
+ let factor = this.system.main.simulationSpeed;
604
+ if (this.system.renderer?.renderMode === ParticleSystemRenderMode.Stretch) {
605
+ factor *= this.system.renderer.velocityScale ?? 1;
606
+ }
607
+ return factor;
608
+ }
604
609
  get texture(): THREE.Texture {
605
610
  const mat = this.material;
606
611
  if (mat && mat["map"]) {
@@ -882,7 +887,7 @@
882
887
  awake(): void {
883
888
  this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as ParticleSystemRenderer;
884
889
 
885
- if (!this.main) {
890
+ if (!this.main) {
886
891
  throw new Error("Not Supported: ParticleSystem needs a serialized MainModule. Creating new particle systems at runtime is currently not supported.");
887
892
  }
888
893
 
@@ -986,7 +991,7 @@
986
991
  private onSimulate(dt: number) {
987
992
  if (this._batchSystem) {
988
993
  let needsUpdate = this.context.time.frameCount % 60 === 0;
989
- if(this._lastBatchesCount !== this._batchSystem.batches.length) {
994
+ if (this._lastBatchesCount !== this._batchSystem.batches.length) {
990
995
  this._lastBatchesCount = this._batchSystem.batches.length;
991
996
  needsUpdate = true;
992
997
  }
src/engine-components/SceneSwitcher.ts CHANGED
@@ -467,8 +467,16 @@
467
467
  }
468
468
  }
469
469
 
470
- private tryGetSceneEventListener(obj: Object3D): ISceneEventListener | null {
470
+ private tryGetSceneEventListener(obj: Object3D, level: number = 0): ISceneEventListener | null {
471
471
  const sceneListener = GameObject.foreachComponent(obj, c => (c as any as ISceneEventListener).sceneClosing ? c : null) as ISceneEventListener | null;
472
+ // if we didnt find any component with the listener on the root object
473
+ // we also check the first level of its children because a scene might be a group
474
+ if (level === 0 && !sceneListener && obj.children.length) {
475
+ for (const ch of obj.children) {
476
+ const res = this.tryGetSceneEventListener(ch, level + 1);
477
+ if (res) return res;
478
+ }
479
+ }
472
480
  return sceneListener;
473
481
  }
474
482
  }
src/engine-components/ui/Text.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { Canvas } from './Canvas.js';
7
7
  import { serializable } from '../../engine/engine_serialization_decorator.js';
8
8
  import { getParam, resolveUrl } from '../../engine/engine_utils.js';
9
- import { ICanvas, IHasAlphaFactor } from './Interfaces.js';
9
+ import { ICanvas, ICanvasEventReceiver, IHasAlphaFactor } from './Interfaces.js';
10
10
 
11
11
  const debug = getParam("debugtext");
12
12
 
@@ -38,7 +38,7 @@
38
38
  BoldAndItalic = 3,
39
39
  }
40
40
 
41
- export class Text extends Graphic implements IHasAlphaFactor {
41
+ export class Text extends Graphic implements IHasAlphaFactor, ICanvasEventReceiver {
42
42
 
43
43
  @serializable()
44
44
  alignment: TextAnchor = TextAnchor.UpperLeft;
@@ -102,11 +102,15 @@
102
102
  }
103
103
  }
104
104
 
105
- onBeforeRender(): void {
106
- // TODO TMUI @swingingtom this is so we don't have text clipping
107
- if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR)) {
108
- this.updateOverflow();
109
- }
105
+ // onBeforeRender(): void {
106
+ // // TODO TMUI @swingingtom this is so we don't have text clipping
107
+ // if (this.uiObject && (this.Canvas?.screenspace || this.context.isInVR)) {
108
+ // this.updateOverflow();
109
+ // }
110
+ // }
111
+ onBeforeCanvasRender(_canvas: ICanvas) {
112
+ // ensure the text clipping matrix is updated (this was a problem with multiple screenspace canvases due to canvas reparenting)
113
+ this.updateOverflow();
110
114
  }
111
115
 
112
116
  private updateOverflow() {
@@ -220,7 +224,12 @@
220
224
  }
221
225
 
222
226
  setTimeout(() => this.markDirty(), 10);
227
+ this.canvas?.registerEventReceiver(this);
223
228
  }
229
+ onDisable(): void {
230
+ super.onDisable();
231
+ this.canvas?.unregisterEventReceiver(this);
232
+ }
224
233
 
225
234
  private getAlignment(opts: ThreeMeshUIEveryOptions): ThreeMeshUIEveryOptions {
226
235
 
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -46,6 +46,7 @@
46
46
  private _isTouching: boolean = false;
47
47
  private _rigStartPose: Matrix4 | undefined | null = null;
48
48
  private _gotFirstHitTestResult: boolean = false;
49
+ private _anchor: XRAnchor | null = null;
49
50
 
50
51
  onBegin(session: XRSession) {
51
52
  this._placementPose = null;
@@ -54,6 +55,7 @@
54
55
  this._startPose = this.gameObject.matrix.clone();
55
56
  this._rigStartPose = this.rig?.matrix.clone();
56
57
  this._gotFirstHitTestResult = false;
58
+ this._anchor = null;
57
59
  session.addEventListener('selectstart', this._selectStartFn);
58
60
  session.addEventListener('selectend', this._selectEndFn);
59
61
  // setTimeout(() => this.gameObject.visible = false, 1000); // TODO test on phone AR and Hololens if this was still needed
@@ -73,8 +75,9 @@
73
75
  this.dispatchEvent(new CustomEvent('onBeginSession'));
74
76
  }
75
77
 
76
- onUpdate(rig: Object3D | null, _session: XRSession, pose: XRPose | null | undefined): boolean {
78
+ onUpdate(rig: Object3D | null, _session: XRSession, hit: XRHitTestResult | null, pose: XRPose | null | undefined): boolean {
77
79
 
80
+
78
81
  if (pose && !this._placementPose) {
79
82
 
80
83
  if (!this._gotFirstHitTestResult) {
@@ -89,6 +92,8 @@
89
92
 
90
93
  if (this.webAR) this.webAR.setReticleActive(false);
91
94
  this.placeAt(rig, poseMatrix);
95
+ if (hit && pose)
96
+ this.onCreatePlacementAnchor(hit, pose);
92
97
  return true;
93
98
  }
94
99
  }
@@ -104,11 +109,34 @@
104
109
  // }
105
110
  }
106
111
 
112
+ private async onCreatePlacementAnchor(hit: XRHitTestResult, pose: XRPose) {
113
+ this._anchor = null;
114
+ hit.createAnchor?.call(hit, pose.transform)?.then(anchor => {
115
+ if (this.context.isInAR)
116
+ this._anchor = anchor;
117
+ });
118
+ }
119
+
120
+ private _anchorMatrix: Matrix4 = new Matrix4();
121
+ onBeforeRender(frame: XRFrame | null): void {
122
+ if (frame && this._anchor && this._rig) {
123
+ const referenceSpace = this.context.renderer.xr.getReferenceSpace();
124
+ if (referenceSpace) {
125
+ const pose = frame.getPose(this._anchor.anchorSpace, referenceSpace);
126
+ if (pose) {
127
+ const poseMatrix = this._anchorMatrix.fromArray(pose.transform.matrix).invert();
128
+ this.placeAt(this._rig, poseMatrix);
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ private _invertedSessionRootMatrix: Matrix4 = new Matrix4();
107
135
  placeAt(rig: Object3D | null, mat: Matrix4) {
108
136
  if (!this._placementPose) this._placementPose = new Matrix4();
109
137
  this._placementPose.copy(mat);
110
138
  // apply session root offset
111
- const invertedSessionRoot = this.gameObject.matrixWorld.clone().invert();
139
+ const invertedSessionRoot = this._invertedSessionRootMatrix.copy(this.gameObject.matrixWorld).invert();
112
140
  this._placementPose.premultiply(invertedSessionRoot);
113
141
  if (rig) {
114
142
 
@@ -132,6 +160,7 @@
132
160
  this._placementPose = null;
133
161
  this.gameObject.visible = false;
134
162
  this.gameObject.matrixAutoUpdate = false;
163
+ this._anchor = null;
135
164
  if (this._startPose) {
136
165
  this.gameObject.matrix.copy(this._startPose);
137
166
  }
@@ -171,9 +200,10 @@
171
200
  }
172
201
  // we apply the transform to the rig because we want to move the user's position for easy networking
173
202
  rig.matrixAutoUpdate = false;
174
- rig.matrix.multiplyMatrices(new Matrix4().makeScale(scale, scale, scale), this._placementPose);
203
+ rig.matrix.multiplyMatrices(tempMatrix.makeScale(scale, scale, scale), this._placementPose);
175
204
  rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
176
205
  rig.updateMatrixWorld();
177
- console.log("Place", rig.position);
178
206
  }
179
- }
207
+ }
208
+
209
+ const tempMatrix = new Matrix4();
src/engine-components/webxr/WebXR.ts CHANGED
@@ -122,6 +122,7 @@
122
122
  options.domOverlay = { root: domOverlayRoot };
123
123
  options.optionalFeatures.push('dom-overlay')
124
124
  options.optionalFeatures.push('hit-test');
125
+ options.optionalFeatures.push('anchors');
125
126
  }
126
127
  else {
127
128
  console.warn("No dom overlay root found, HTML overlays on top of screen-based AR will not work.");
@@ -712,7 +713,7 @@
712
713
  const pose = hit.getPose(referenceSpace);
713
714
 
714
715
  if (this.sessionRoot) {
715
- const didPlace = this.sessionRoot.onUpdate(this.webxr.Rig, session, pose);
716
+ const didPlace = this.sessionRoot.onUpdate(this.webxr.Rig, session, hit, pose);
716
717
  this.didPlaceARSessionRoot = didPlace;
717
718
  }
718
719
 
@@ -730,7 +731,7 @@
730
731
  }
731
732
 
732
733
  } else {
733
- this.sessionRoot?.onUpdate(this.webxr.Rig, session, null);
734
+ this.sessionRoot?.onUpdate(this.webxr.Rig, session, null, null);
734
735
  if (this.reticle)
735
736
  this.reticle.visible = false;
736
737
  }