Needle Engine

Changes between version 3.32.13-alpha and 3.32.14-alpha
Files changed (11) hide show
  1. src/engine-components/api.ts +1 -1
  2. src/engine-components/AudioSource.ts +5 -2
  3. src/engine/debug/debug.ts +4 -0
  4. src/engine/engine_element_loading.ts +17 -5
  5. src/engine/engine_utils.ts +0 -1
  6. src/engine/xr/NeedleXRController.ts +19 -8
  7. src/engine-components/ParticleSystem.ts +1 -1
  8. src/engine-components/ParticleSystemModules.ts +140 -8
  9. src/engine-components/Renderer.ts +29 -2
  10. src/engine-components/webxr/controllers/XRControllerModel.ts +8 -1
  11. src/engine-components/webxr/controllers/XRControllerMovement.ts +11 -8
src/engine-components/api.ts CHANGED
@@ -17,4 +17,4 @@
17
17
  import "./AnimationUtils.js"
18
18
 
19
19
  export { ParticleSystemBaseBehaviour, type QParticle, type QParticleBehaviour } from "./ParticleSystem.js"
20
-
20
+ export { ParticleSystemShapeType } from "./ParticleSystemModules.js"
src/engine-components/AudioSource.ts CHANGED
@@ -327,7 +327,7 @@
327
327
  }
328
328
  }
329
329
 
330
- private onNewClip(clip?: string | MediaStream) {
330
+ private async onNewClip(clip?: string | MediaStream) {
331
331
  if (clip) this.clip = clip;
332
332
  if (typeof clip === "string") {
333
333
  if (debug)
@@ -343,7 +343,10 @@
343
343
  this._lastClipStartedLoading = clip;
344
344
  if (debug)
345
345
  console.log("load audio", clip);
346
- this.audioLoader.load(clip, this.createAudio, () => { }, console.error);
346
+ const buffer = await this.audioLoader.loadAsync(clip).catch(console.error);
347
+ this._lastClipStartedLoading = null;
348
+ if (buffer)
349
+ this.createAudio(buffer);
347
350
  }
348
351
  else console.warn("Unsupported audio clip type", clip)
349
352
  }
src/engine/debug/debug.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { isLocalNetwork } from "../engine_networking_utils.js";
2
+ import { getParam } from "../engine_utils.js";
2
3
  import { showDebugConsole } from "./debug_console.js";
3
4
  import { addLog, LogType, setAllowOverlayMessages } from "./debug_overlay.js";
4
5
 
5
6
  export { showDebugConsole }
6
7
  export { LogType, setAllowOverlayMessages };
7
8
 
9
+ const noDevLogs = getParam("nodevlogs");
10
+
8
11
  /** Displays a debug message on screen for a certain amount of time */
9
12
  export function showBalloonMessage(text: string, logType: LogType = LogType.Log): void {
10
13
  addLog(logType, text);
@@ -22,6 +25,7 @@
22
25
 
23
26
  /** True when the application runs on a local url */
24
27
  export function isDevEnvironment(): boolean {
28
+ if (noDevLogs) return false;
25
29
  if (_manuallySetDevEnvironment !== undefined) return _manuallySetDevEnvironment;
26
30
  return isLocalNetwork();
27
31
  }
src/engine/engine_element_loading.ts CHANGED
@@ -228,6 +228,18 @@
228
228
  }
229
229
  }
230
230
 
231
+ const container = document.createElement("div");
232
+ container.style.cssText = `
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: center;
236
+ justify-content: center;
237
+ width: 100%;
238
+ opacity: 0;
239
+ transition: opacity 1.2s ease-in-out .2s;
240
+ `;
241
+ setTimeout(() => { container.style.opacity = "1"; }, 1);
242
+ this._loadingElement.appendChild(container);
231
243
 
232
244
  const loadingBarContainer = document.createElement("div");
233
245
  const maxWidth = 30;
@@ -270,8 +282,8 @@
270
282
  logo.style.pointerEvents = "all";
271
283
  logo.addEventListener("click", () => window.open("https://needle.tools", "_blank"));
272
284
  }
273
- this._loadingElement.appendChild(logo);
274
- this._loadingElement.appendChild(loadingBarContainer);
285
+ container.appendChild(logo);
286
+ container.appendChild(loadingBarContainer);
275
287
 
276
288
 
277
289
  this._loadingBar = document.createElement("div");
@@ -302,7 +314,7 @@
302
314
  this._loadingTextContainer.style.display = "flex";
303
315
  this._loadingTextContainer.style.justifyContent = "center";
304
316
  this._loadingTextContainer.style.marginTop = "1.2em";
305
- this._loadingElement.appendChild(this._loadingTextContainer);
317
+ container.appendChild(this._loadingTextContainer);
306
318
 
307
319
  const messageContainer = document.createElement("div");
308
320
  this._messageContainer = messageContainer;
@@ -312,7 +324,7 @@
312
324
  messageContainer.style.fontWeight = "200";
313
325
  // messageContainer.style.border = "1px solid rgba(255,255,255,.1)";
314
326
  messageContainer.style.justifyContent = "center";
315
- this._loadingElement.appendChild(messageContainer);
327
+ container.appendChild(messageContainer);
316
328
 
317
329
  if (hasLicense && this._element) {
318
330
  const loadingTextColor = this._element.getAttribute("loading-text-color");
@@ -321,7 +333,7 @@
321
333
  }
322
334
  }
323
335
 
324
- this.handleRuntimeLicense(this._loadingElement);
336
+ this.handleRuntimeLicense(container);
325
337
 
326
338
  return this._loadingElement;
327
339
  }
src/engine/engine_utils.ts CHANGED
@@ -674,6 +674,5 @@
674
674
  correctLevel: QRCODE.CorrectLevel.M,
675
675
  ...args,
676
676
  });
677
- console.log("QRCode generated for " + args.text);
678
677
  return target;
679
678
  }
src/engine/xr/NeedleXRController.ts CHANGED
@@ -157,17 +157,32 @@
157
157
  }
158
158
  private readonly _gripWorldQuaternion: Quaternion = new Quaternion();
159
159
 
160
- /** Controller ray position in worldspace */
160
+ /** Controller ray position in worldspace (this value is calculated once per frame by default - call `updateRayWorldPosition` to force an update) */
161
161
  get rayWorldPosition() {
162
162
  return getTempVector(this._rayWorldPosition);
163
163
  }
164
164
  private readonly _rayWorldPosition: Vector3 = new Vector3();
165
+ /** Recalculates the ray world position */
166
+ updateRayWorldPosition() {
167
+ const parent = this.xr.context.mainCamera?.parent;
168
+ this._rayWorldPosition.copy(this._rayPosition);
169
+ if (parent) this._rayWorldPosition.applyMatrix4(parent.matrixWorld);
170
+ }
165
171
 
166
- /** Controller ray rotation in wordspace */
172
+ /** Controller ray rotation in wordspace (this value is calculated once per frame by default - call `updateRayWorldQuaternion` to force an update) */
167
173
  get rayWorldQuaternion() {
168
174
  return getTempQuaternion(this._rayWorldQuaternion);
169
175
  }
170
176
  private readonly _rayWorldQuaternion: Quaternion = new Quaternion();
177
+ /** Recalculates the ray world quaternion */
178
+ updateRayWorldQuaternion() {
179
+ const parent = this.xr.context.mainCamera?.parent;
180
+ const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
181
+ this._rayWorldQuaternion.copy(this._rayQuaternion)
182
+ // flip forward because we want +Z to be forward
183
+ .multiply(flipForwardQuaternion);
184
+ if (parentWorldQuaternion) this._rayWorldQuaternion.premultiply(parentWorldQuaternion)
185
+ }
171
186
 
172
187
  /** The controller ray in worldspace */
173
188
  get ray(): Ray {
@@ -324,12 +339,8 @@
324
339
  if (parentWorldQuaternion) this._gripWorldQuaternion.premultiply(parentWorldQuaternion)
325
340
 
326
341
  // RAY
327
- this._rayWorldPosition.copy(this._rayPosition);
328
- if (parent) this._rayWorldPosition.applyMatrix4(parent.matrixWorld);
329
- this._rayWorldQuaternion.copy(this._rayQuaternion)
330
- // flip forward because we want +Z to be forward
331
- .multiply(flipForwardQuaternion);
332
- if (parentWorldQuaternion) this._rayWorldQuaternion.premultiply(parentWorldQuaternion)
342
+ this.updateRayWorldPosition();
343
+ this.updateRayWorldQuaternion();
333
344
  }
334
345
 
335
346
  /** Called when the input source disconnects */
src/engine-components/ParticleSystem.ts CHANGED
@@ -429,7 +429,7 @@
429
429
  const simulationSpeed = this.system.main.simulationSpeed;
430
430
 
431
431
  particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random());
432
- particle.velocity.copy(this.system.shape.getDirection(particle.position)).multiplyScalar(particle.startSpeed);
432
+ particle.velocity.copy(this.system.shape.getDirection(particle, particle.position)).multiplyScalar(particle.startSpeed);
433
433
  if (this.system.inheritVelocity?.enabled) {
434
434
  this.system.inheritVelocity.applyInitial(particle.velocity);
435
435
  }
src/engine-components/ParticleSystemModules.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
2
- import { Euler, Matrix4, Object3D, Quaternion, Vector2, Vector3, Vector4 } from "three";
2
+ import { BufferGeometry, Euler, Matrix4, Mesh, Object3D, Quaternion, Triangle, Vector2, Vector3, Vector4 } from "three";
3
3
  import type { EmitterShape, Particle, ShapeJSON } from "three.quarks";
4
4
 
5
+ import { isDevEnvironment } from '../engine/debug/index.js';
5
6
  import { Gizmos } from "../engine/engine_gizmos.js";
6
7
  import { Mathf } from "../engine/engine_math.js";
7
8
  import { serializable } from "../engine/engine_serialization.js";
8
9
  import { Context } from "../engine/engine_setup.js";
10
+ import { getTempVector, getWorldQuaternion } from '../engine/engine_three_utils.js';
9
11
  import type { Vec2, Vec3 } from "../engine/engine_types.js";
10
12
  import { getParam } from "../engine/engine_utils.js";
11
13
  import { AnimationCurve } from "./AnimationCurve.js";
12
14
  import { RGBAColor } from "./js-extensions/RGBAColor.js";
15
+ import { MeshRenderer } from './Renderer.js';
13
16
 
14
17
  const debug = getParam("debugparticles");
15
18
 
@@ -500,6 +503,13 @@
500
503
  }
501
504
  }
502
505
 
506
+
507
+ export enum ParticleSystemMeshShapeType {
508
+ Vertex = 0,
509
+ Edge = 1,
510
+ Triangle = 2,
511
+ }
512
+
503
513
  export class ShapeModule implements EmitterShape {
504
514
 
505
515
  // Emittershape start
@@ -507,7 +517,7 @@
507
517
  return ParticleSystemShapeType[this.shapeType];
508
518
  }
509
519
  initialize(particle: Particle): void {
510
- this.getPosition();
520
+ this.onInitialize(particle);
511
521
  particle.position.copy(this._vector);
512
522
  }
513
523
  toJSON(): ShapeJSON {
@@ -557,6 +567,30 @@
557
567
  @serializable()
558
568
  randomPositionAmount!: number;
559
569
 
570
+ /** Controls if particles should spawn off vertices, faces or edges. `shapeType` must be set to `MeshRenderer` */
571
+ @serializable()
572
+ meshShapeType?: ParticleSystemMeshShapeType;
573
+ /** When assigned and `shapeType` is set to `MeshRenderer` particles will spawn using a mesh in the scene.
574
+ * Use the `meshShapeType` to choose if particles should be spawned from vertices, faces or edges
575
+ * To re-assign use the `setMesh` function to cache the mesh and geometry
576
+ * */
577
+ @serializable(MeshRenderer)
578
+ meshRenderer?: MeshRenderer;
579
+
580
+ private _meshObj?: Mesh;
581
+ private _meshGeometry?: BufferGeometry;
582
+ setMesh(mesh: MeshRenderer) {
583
+ this.meshRenderer = mesh;
584
+ if (mesh) {
585
+ this._meshObj = mesh.sharedMeshes[Math.floor(Math.random() * mesh.sharedMeshes.length)];
586
+ this._meshGeometry = this._meshObj.geometry;
587
+ }
588
+ else {
589
+ this._meshObj = undefined;
590
+ this._meshGeometry = undefined;
591
+ }
592
+ }
593
+
560
594
  private system!: IParticleSystem;
561
595
  private _space?: ParticleSystemSimulationSpace;
562
596
  private readonly _worldSpaceMatrix: Matrix4 = new Matrix4();
@@ -607,12 +641,13 @@
607
641
  /** initializer implementation */
608
642
  private _vector: Vector3 = new Vector3(0, 0, 0);
609
643
  private _temp: Vector3 = new Vector3(0, 0, 0);
610
- /** called by nebula on initialize */
611
- get vector() {
612
- return this._vector;
613
- }
614
- getPosition(): void {
644
+ private _triangle: Triangle = new Triangle();
645
+
646
+ onInitialize(particle: Particle): void {
615
647
  this._vector.set(0, 0, 0);
648
+ // remove mesh from particle in case it got destroyed (we dont want to keep a reference to a destroyed mesh in the particle system)
649
+ particle["mesh"] = undefined;
650
+ particle["mesh_geometry"] = undefined;
616
651
 
617
652
  const pos = this._temp.copy(this.position);
618
653
  const isWorldSpace = this._space === ParticleSystemSimulationSpace.World;
@@ -639,8 +674,64 @@
639
674
  case ParticleSystemShapeType.Circle:
640
675
  this.randomCirclePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
641
676
  break;
677
+ case ParticleSystemShapeType.MeshRenderer:
678
+ const renderer = this.meshRenderer;
679
+ if (renderer?.destroyed == false) this.setMesh(renderer);
680
+ const mesh = particle["mesh"] = this._meshObj;
681
+ const geometry = particle["mesh_geometry"] = this._meshGeometry;
682
+ if (mesh && geometry) {
683
+ switch (this.meshShapeType) {
684
+ case ParticleSystemMeshShapeType.Vertex:
685
+ {
686
+ const vertices = geometry.getAttribute("position");
687
+ const index = Math.floor(Math.random() * vertices.count);
688
+ this._vector.fromBufferAttribute(vertices, index);
689
+ this._vector.applyMatrix4(mesh.matrixWorld);
690
+ particle["mesh_normal"] = index;
691
+ }
692
+ break;
693
+ case ParticleSystemMeshShapeType.Edge:
694
+ break;
695
+ case ParticleSystemMeshShapeType.Triangle:
696
+ {
697
+ const faces = geometry.index;
698
+ if (faces) {
699
+ let u = Math.random();
700
+ let v = Math.random();
701
+ if (u + v > 1) {
702
+ u = 1 - u;
703
+ v = 1 - v;
704
+ }
705
+ const faceIndex = Math.floor(Math.random() * (faces.count / 3));
706
+ let i0 = faceIndex * 3;
707
+ let i1 = faceIndex * 3 + 1;
708
+ let i2 = faceIndex * 3 + 2;
709
+ i0 = faces.getX(i0);
710
+ i1 = faces.getX(i1);
711
+ i2 = faces.getX(i2);
712
+ const positionAttribute = geometry.getAttribute("position");
713
+ this._triangle.a.fromBufferAttribute(positionAttribute, i0);
714
+ this._triangle.b.fromBufferAttribute(positionAttribute, i1);
715
+ this._triangle.c.fromBufferAttribute(positionAttribute, i2);
716
+ this._vector
717
+ .set(0, 0, 0)
718
+ .addScaledVector(this._triangle.a, u)
719
+ .addScaledVector(this._triangle.b, v)
720
+ .addScaledVector(this._triangle.c, 1 - (u + v));
721
+ this._vector.applyMatrix4(mesh.matrixWorld);
722
+ particle["mesh_normal"] = faceIndex;
723
+ }
724
+ }
725
+ break;
726
+ }
727
+ }
728
+ break;
642
729
  default:
643
730
  this._vector.set(0, 0, 0);
731
+ if (isDevEnvironment() && !globalThis["__particlesystem_shapetype_unsupported"]) {
732
+ console.warn("ParticleSystem ShapeType is not supported:", ParticleSystemShapeType[this.shapeType]);
733
+ globalThis["__particlesystem_shapetype_unsupported"] = true;
734
+ }
644
735
  break;
645
736
  // case ParticleSystemShapeType.Hemisphere:
646
737
  // randomSpherePoint(this.position.x, this.position.y, this.position.z, this.radius, this.radiusThickness, 180, this._vector);
@@ -666,7 +757,7 @@
666
757
 
667
758
  private _dir: Vector3 = new Vector3();
668
759
 
669
- getDirection(pos: Vec3): Vector3 {
760
+ getDirection(particle: Particle, pos: Vec3): Vector3 {
670
761
  if (!this.enabled) {
671
762
  this._dir.set(0, 0, 1);
672
763
  return this._dir;
@@ -691,6 +782,47 @@
691
782
  else
692
783
  this._dir.sub(this.position)
693
784
  break;
785
+ case ParticleSystemShapeType.MeshRenderer:
786
+ const mesh = particle["mesh"];
787
+ const geometry = particle["mesh_geometry"];
788
+ if (mesh && geometry) {
789
+ switch (this.meshShapeType) {
790
+ case ParticleSystemMeshShapeType.Vertex:
791
+ {
792
+ const normal = geometry.getAttribute("normal");
793
+ const index = particle["mesh_normal"];
794
+ this._dir.fromBufferAttribute(normal, index);
795
+ }
796
+ break;
797
+ case ParticleSystemMeshShapeType.Edge:
798
+ break;
799
+ case ParticleSystemMeshShapeType.Triangle:
800
+ {
801
+ const faces = geometry.index;
802
+ if (faces) {
803
+ const index = particle["mesh_normal"];
804
+ const i0 = faces.getX(index * 3);
805
+ const i1 = faces.getX(index * 3 + 1);
806
+ const i2 = faces.getX(index * 3 + 2);
807
+ const positionAttribute = geometry.getAttribute("position");
808
+ const a = getTempVector();
809
+ const b = getTempVector();
810
+ const c = getTempVector();
811
+ a.fromBufferAttribute(positionAttribute, i0);
812
+ b.fromBufferAttribute(positionAttribute, i1);
813
+ c.fromBufferAttribute(positionAttribute, i2);
814
+ a.sub(b);
815
+ c.sub(b);
816
+ a.cross(c);
817
+ this._dir.copy(a).multiplyScalar(-1);
818
+ const rot = getWorldQuaternion(mesh);
819
+ this._dir.applyQuaternion(rot)
820
+ }
821
+ }
822
+ break;
823
+ }
824
+ }
825
+ break;
694
826
  default:
695
827
  this._dir.set(0, 0, 1);
696
828
  break;
src/engine-components/Renderer.ts CHANGED
@@ -241,7 +241,11 @@
241
241
  // private _materialProperties: Array<MaterialProperties> | undefined = undefined;
242
242
  private _lightmaps?: RendererLightmap[];
243
243
 
244
- get sharedMesh(): Mesh | undefined {
244
+ /** Get the mesh Object3D for this renderer
245
+ * Warn: if this is a multimaterial object it will return the first mesh only
246
+ * @returns the mesh object3D.
247
+ * */
248
+ get sharedMesh(): Mesh | SkinnedMesh | undefined {
245
249
  if (this.gameObject.type === "Mesh") {
246
250
  return this.gameObject as unknown as Mesh
247
251
  }
@@ -254,6 +258,26 @@
254
258
  return undefined;
255
259
  }
256
260
 
261
+ private readonly _sharedMeshes: Mesh[] = [];
262
+ /** Get all the mesh Object3D for this renderer
263
+ * @returns an array of mesh object3D.
264
+ */
265
+ get sharedMeshes(): Mesh[] {
266
+ if (this.destroyed || !this.gameObject) return this._sharedMeshes;
267
+ this._sharedMeshes.length = 0;
268
+ if (this.gameObject.type === "Group") {
269
+ for (const ch of this.gameObject.children) {
270
+ if (ch.type === "Mesh" || ch.type === "SkinnedMesh") {
271
+ this._sharedMeshes.push(ch as Mesh);
272
+ }
273
+ }
274
+ }
275
+ else if (this.gameObject.type === "Mesh" || this.gameObject.type === "SkinnedMesh") {
276
+ this._sharedMeshes.push(this.gameObject as unknown as Mesh);
277
+ }
278
+ return this._sharedMeshes;
279
+ }
280
+
257
281
  get sharedMaterial(): Material {
258
282
  return this.sharedMaterials[0];
259
283
  }
@@ -518,6 +542,9 @@
518
542
  }
519
543
 
520
544
  onEnable() {
545
+ // ensure shared meshes are initialized
546
+ const _ = this.sharedMeshes;
547
+
521
548
  this.setVisibility(true);
522
549
 
523
550
  if (this._isInstancingEnabled) {
@@ -1009,7 +1036,7 @@
1009
1036
  material.defines["USE_INSTANCING"] = true;
1010
1037
  material.needsUpdate = true;
1011
1038
  }
1012
-
1039
+
1013
1040
  context.pre_render_callbacks.push(this.onBeforeRender);
1014
1041
  context.post_render_callbacks.push(this.onAfterRender);
1015
1042
  }
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -53,7 +53,11 @@
53
53
  if (controller.hand) {
54
54
  if (this.createHandModel) {
55
55
  const res = await this.loadHandModel(controller);
56
- if (!res || !controller.connected) return;
56
+ if (!res || !controller.connected) {
57
+ res?.handObject?.removeFromParent();
58
+ res?.handmesh?.controller?.removeFromParent();
59
+ return;
60
+ }
57
61
  this._models[controller.index] = { controller: controller, model: res.handObject, handmesh: res.handmesh };
58
62
  this.scene.add(res.handObject);
59
63
  }
@@ -215,6 +219,9 @@
215
219
  if (NeedleXRSession.active?.isPassThrough)
216
220
  this.makeOccluder(child);
217
221
  });
222
+ if (!controller.connected) {
223
+ object.removeFromParent();
224
+ }
218
225
  });
219
226
 
220
227
  if (debug) handObject.add(new AxesHelper(.5));
src/engine-components/webxr/controllers/XRControllerMovement.ts CHANGED
@@ -60,10 +60,6 @@
60
60
 
61
61
  // in AR pass through mode we dont want to move the rig
62
62
  if (args.xr.isPassThrough) {
63
- if (this.showRays)
64
- this.renderRays(args.xr);
65
- if (this.showHits)
66
- this.renderHits(args.xr);
67
63
  return;
68
64
  }
69
65
 
@@ -78,10 +74,6 @@
78
74
  this.onHandleTeleport(teleportController, rig.gameObject);
79
75
  }
80
76
 
81
- if (this.showRays)
82
- this.renderRays(args.xr);
83
- if (this.showHits)
84
- this.renderHits(args.xr);
85
77
  }
86
78
  onLeaveXR(_: NeedleXREventArgs): void {
87
79
  for (const line of this._lines) {
@@ -92,6 +84,15 @@
92
84
  }
93
85
  }
94
86
 
87
+ onBeforeRender(): void {
88
+ if (this.context.xr?.running) {
89
+ if (this.showRays)
90
+ this.renderRays(this.context.xr);
91
+ if (this.showHits)
92
+ this.renderHits(this.context.xr);
93
+ }
94
+ }
95
+
95
96
  protected onHandleMovement(controller: NeedleXRController, rig: IGameObject) {
96
97
  const stick = controller.getStick("xr-standard-thumbstick");
97
98
  const vec = new Vector3(stick.x, 0, stick.y);
@@ -185,6 +186,8 @@
185
186
  this._lines[i] = line;
186
187
  }
187
188
 
189
+ ctrl.updateRayWorldPosition();
190
+ ctrl.updateRayWorldQuaternion();
188
191
  const pos = ctrl.rayWorldPosition;
189
192
  const rot = ctrl.rayWorldQuaternion;
190
193
  line.position.copy(pos);