@needle-tools/engine 2.65.2-pre → 2.66.1-pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/needle-engine.js +7866 -7756
  3. package/dist/needle-engine.umd.cjs +223 -223
  4. package/lib/engine/debug/debug_overlay.js +4 -1
  5. package/lib/engine/debug/debug_overlay.js.map +1 -1
  6. package/lib/engine/engine_addressables.js +2 -2
  7. package/lib/engine/engine_addressables.js.map +1 -1
  8. package/lib/engine/engine_element.d.ts +1 -0
  9. package/lib/engine/engine_element.js +4 -1
  10. package/lib/engine/engine_element.js.map +1 -1
  11. package/lib/engine/engine_element_loading.d.ts +3 -2
  12. package/lib/engine/engine_element_loading.js +18 -14
  13. package/lib/engine/engine_element_loading.js.map +1 -1
  14. package/lib/engine/engine_gameobject.js +6 -3
  15. package/lib/engine/engine_gameobject.js.map +1 -1
  16. package/lib/engine/engine_gizmos.js +3 -1
  17. package/lib/engine/engine_gizmos.js.map +1 -1
  18. package/lib/engine/engine_networking.d.ts +3 -1
  19. package/lib/engine/engine_networking.js +10 -8
  20. package/lib/engine/engine_networking.js.map +1 -1
  21. package/lib/engine/engine_physics.d.ts +29 -1
  22. package/lib/engine/engine_physics.js +99 -10
  23. package/lib/engine/engine_physics.js.map +1 -1
  24. package/lib/engine/engine_setup.js +3 -0
  25. package/lib/engine/engine_setup.js.map +1 -1
  26. package/lib/engine/extensions/NEEDLE_render_objects.js +9 -0
  27. package/lib/engine/extensions/NEEDLE_render_objects.js.map +1 -1
  28. package/lib/engine-components/Animator.js +0 -1
  29. package/lib/engine-components/Animator.js.map +1 -1
  30. package/lib/engine-components/CharacterController.d.ts +1 -0
  31. package/lib/engine-components/CharacterController.js +14 -9
  32. package/lib/engine-components/CharacterController.js.map +1 -1
  33. package/lib/engine-components/Collider.js +14 -1
  34. package/lib/engine-components/Collider.js.map +1 -1
  35. package/lib/engine-components/ParticleSystem.d.ts +5 -1
  36. package/lib/engine-components/ParticleSystem.js +41 -6
  37. package/lib/engine-components/ParticleSystem.js.map +1 -1
  38. package/lib/engine-components/ParticleSystemModules.d.ts +2 -0
  39. package/lib/engine-components/ParticleSystemModules.js +26 -0
  40. package/lib/engine-components/ParticleSystemModules.js.map +1 -1
  41. package/lib/engine-components/ParticleSystemSubEmitter.js +5 -2
  42. package/lib/engine-components/ParticleSystemSubEmitter.js.map +1 -1
  43. package/lib/engine-components/Renderer.js +9 -5
  44. package/lib/engine-components/Renderer.js.map +1 -1
  45. package/lib/engine-components/ScreenCapture.js +3 -3
  46. package/lib/engine-components/ScreenCapture.js.map +1 -1
  47. package/lib/engine-components/SpectatorCamera.js +3 -3
  48. package/lib/engine-components/SpectatorCamera.js.map +1 -1
  49. package/lib/engine-components/SyncedCamera.js +1 -1
  50. package/lib/engine-components/SyncedCamera.js.map +1 -1
  51. package/lib/engine-components/SyncedTransform.js +2 -2
  52. package/lib/engine-components/SyncedTransform.js.map +1 -1
  53. package/lib/engine-components/TestRunner.js +1 -1
  54. package/lib/engine-components/TestRunner.js.map +1 -1
  55. package/lib/engine-components/WebARSessionRoot.js +3 -2
  56. package/lib/engine-components/WebARSessionRoot.js.map +1 -1
  57. package/lib/engine-components/WebXRAvatar.js.map +1 -1
  58. package/lib/engine-components/WebXRGrabRendering.js +2 -2
  59. package/lib/engine-components/WebXRGrabRendering.js.map +1 -1
  60. package/lib/engine-components/WebXRSync.js +2 -2
  61. package/lib/engine-components/WebXRSync.js.map +1 -1
  62. package/lib/engine-components-experimental/networking/PlayerSync.js +1 -1
  63. package/lib/engine-components-experimental/networking/PlayerSync.js.map +1 -1
  64. package/package.json +1 -1
  65. package/plugins/vite/meta.js +3 -0
  66. package/plugins/vite/poster-client.js +6 -4
  67. package/src/engine/debug/debug_overlay.ts +4 -1
  68. package/src/engine/engine_addressables.ts +2 -2
  69. package/src/engine/engine_element.ts +8 -1
  70. package/src/engine/engine_element_loading.ts +18 -14
  71. package/src/engine/engine_gameobject.ts +584 -583
  72. package/src/engine/engine_gizmos.ts +3 -2
  73. package/src/engine/engine_networking.ts +10 -8
  74. package/src/engine/engine_physics.ts +113 -10
  75. package/src/engine/engine_setup.ts +4 -0
  76. package/src/engine/extensions/NEEDLE_render_objects.ts +10 -1
  77. package/src/engine-components/Animator.ts +0 -1
  78. package/src/engine-components/CharacterController.ts +12 -9
  79. package/src/engine-components/Collider.ts +16 -2
  80. package/src/engine-components/ParticleSystem.ts +45 -9
  81. package/src/engine-components/ParticleSystemModules.ts +28 -1
  82. package/src/engine-components/ParticleSystemSubEmitter.ts +5 -3
  83. package/src/engine-components/Renderer.ts +14 -11
  84. package/src/engine-components/ScreenCapture.ts +3 -3
  85. package/src/engine-components/SpectatorCamera.ts +3 -3
  86. package/src/engine-components/SyncedCamera.ts +1 -1
  87. package/src/engine-components/SyncedTransform.ts +2 -2
  88. package/src/engine-components/TestRunner.ts +1 -1
  89. package/src/engine-components/WebARSessionRoot.ts +3 -2
  90. package/src/engine-components/WebXRAvatar.ts +0 -1
  91. package/src/engine-components/WebXRGrabRendering.ts +2 -2
  92. package/src/engine-components/WebXRSync.ts +2 -2
  93. package/src/engine-components-experimental/networking/PlayerSync.ts +1 -1
@@ -145,10 +145,9 @@ class Internal {
145
145
  }
146
146
 
147
147
  static getSphere(radius: number, duration: number, wireframe: boolean): Mesh {
148
-
149
148
  let sphere = this.spheresCache.pop();
150
149
  if (!sphere) {
151
- sphere = new Mesh(new SphereGeometry(.5, 8, 8));
150
+ sphere = new Mesh(new SphereGeometry(1, 8, 8));
152
151
  }
153
152
  sphere.scale.set(radius, radius, radius);
154
153
  sphere.material["wireframe"] = wireframe;
@@ -176,6 +175,8 @@ class Internal {
176
175
  this.contextPostRenderCallbacks.set(context, cb);
177
176
  context.post_render_callbacks.push(cb);
178
177
  }
178
+ object.layers.disableAll();
179
+ object.layers.enable(2);
179
180
  object[$cacheSymbol] = cache;
180
181
  this.timedObjectsBuffer.push(object);
181
182
  this.timesBuffer.push(Context.Current.time.time + duration);
@@ -147,7 +147,7 @@ export class OwnershipModel {
147
147
  // console.log(res);
148
148
  if (res.guid === this.guid) {
149
149
  if (this._isWaitingForOwnershipResponseCallback) {
150
- this.connection.stopListening(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
150
+ this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
151
151
  this._isWaitingForOwnershipResponseCallback = null;
152
152
  }
153
153
  this._isOwned = res.value;
@@ -185,18 +185,18 @@ export class OwnershipModel {
185
185
  // TODO: abort "requestOwnershipIfNotOwned"
186
186
  this.connection.send(OwnershipEvent.RemoveOwnership, { guid: this.guid });
187
187
  if (this._isWaitingForOwnershipResponseCallback) {
188
- this.connection.stopListening(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
188
+ this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
189
189
  this._isWaitingForOwnershipResponseCallback = null;
190
190
  }
191
191
  return this;
192
192
  }
193
193
 
194
194
  public destroy() {
195
- this.connection.stopListening(OwnershipEvent.GainedOwnership, this._gainSubscription);
196
- this.connection.stopListening(OwnershipEvent.LostOwnership, this._lostSubscription);
197
- this.connection.stopListening(OwnershipEvent.ResponseHasOwner, this._hasOwnerResponse);
195
+ this.connection.stopListen(OwnershipEvent.GainedOwnership, this._gainSubscription);
196
+ this.connection.stopListen(OwnershipEvent.LostOwnership, this._lostSubscription);
197
+ this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._hasOwnerResponse);
198
198
  if (this._isWaitingForOwnershipResponseCallback) {
199
- this.connection.stopListening(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
199
+ this.connection.stopListen(OwnershipEvent.ResponseHasOwner, this._isWaitingForOwnershipResponseCallback);
200
200
  this._isWaitingForOwnershipResponseCallback = null;
201
201
  }
202
202
  }
@@ -368,7 +368,9 @@ export class NetworkConnection implements INetworkConnection {
368
368
  return callback;
369
369
  }
370
370
 
371
- public stopListening(key: string | OwnershipEvent, callback: Function | null) {
371
+ /**@deprecated please use stopListen instead (2.65.2-pre) */
372
+ public stopListening(key: string | OwnershipEvent, callback: Function | null) { return this.stopListen(key, callback); }
373
+ public stopListen(key: string | OwnershipEvent, callback: Function | null) {
372
374
  if (!callback) return;
373
375
  if (!this._listeners[key]) return;
374
376
  const index = this._listeners[key].indexOf(callback);
@@ -377,7 +379,7 @@ export class NetworkConnection implements INetworkConnection {
377
379
  }
378
380
  }
379
381
 
380
- public beginListenBinrary(identifier: string, callback: BinaryCallback): BinaryCallback {
382
+ public beginListenBinary(identifier: string, callback: BinaryCallback): BinaryCallback {
381
383
  if (!this._listenersBinary[identifier])
382
384
  this._listenersBinary[identifier] = [];
383
385
  this._listenersBinary[identifier].push(callback);
@@ -15,7 +15,7 @@ import {
15
15
  import { InstancingUtil } from './engine_instancing';
16
16
  import { foreachComponent } from './engine_gameobject';
17
17
 
18
- import RAPIER, { ActiveEvents, CoefficientCombineRule, Collider, ColliderDesc, EventQueue, JointData, RigidBody, RigidBodyType, World } from '@dimforge/rapier3d-compat';
18
+ import RAPIER, { ActiveEvents, CoefficientCombineRule, Collider, ColliderDesc, EventQueue, JointData, QueryFilterFlags, RigidBody, RigidBodyType, ShapeColliderTOI, World } from '@dimforge/rapier3d-compat';
19
19
  import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types';
20
20
  import { Gizmos } from './engine_gizmos';
21
21
  import { Mathf } from './engine_math';
@@ -83,12 +83,22 @@ export class SphereIntersection implements Intersection {
83
83
  }
84
84
  }
85
85
 
86
+ export class SphereOverlapResult {
87
+ object: Object3D;
88
+ collider: ICollider;
89
+ constructor(object: Object3D, collider: ICollider) {
90
+ this.object = object;
91
+ this.collider = collider;
92
+ }
93
+ }
94
+
86
95
  declare type PhysicsRaycastResult = {
87
96
  point: Vector3,
88
97
  normal?: Vector3,
89
98
  collider?: ICollider
90
99
  }
91
100
 
101
+
92
102
  export class Physics {
93
103
 
94
104
  // raycasting
@@ -107,6 +117,12 @@ export class Physics {
107
117
 
108
118
  private sphereResults: Array<Intersection> = new Array<Intersection>();
109
119
  private sphereMask: Layers = new Layers();
120
+ /** Test overlapping of a sphere with the threejs geometry. This does not use colliders. This does not return an exact intersection point (intersections returned contain the object and the world position of the object that is being hit)
121
+ * For a more accurate test use the physics engine's collider overlap test (see sphereOverlapPhysics)
122
+ * @param spherePos the center of the sphere in world space
123
+ * @param radius the radius of the sphere
124
+ * @param traverseChildsAfterHit if false it will stop after the first hit. If true it will continue to traverse and add all hits to the result array
125
+ */
110
126
  public sphereOverlap(spherePos: Vector3, radius: number, traverseChildsAfterHit: boolean = true): Array<Intersection> {
111
127
  this.sphereResults.length = 0;
112
128
  if (!this.context.scene) return this.sphereResults;
@@ -130,10 +146,9 @@ export class Physics {
130
146
  if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
131
147
  const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
132
148
  if (sp.intersectsBox(test)) {
133
- // console.log(obj, obj.layers.test(mask), obj.layers.mask, mask.mask);
134
149
  const wp = getWorldPosition(obj);
135
150
  const dist = wp.distanceTo(sp.center);
136
- const int = new SphereIntersection(obj, dist, sp.center.clone());
151
+ const int = new SphereIntersection(obj, dist, wp);
137
152
  results.push(int);
138
153
  if (!traverseChildsAfterHit) return;
139
154
  }
@@ -157,6 +172,8 @@ export class Physics {
157
172
  /** raycast against rendered three objects. This might be very slow depending on your scene complexity.
158
173
  * We recommend setting objects to IgnoreRaycast layer (2) when you don't need them to be raycasted.
159
174
  * Raycasting SkinnedMeshes is specially expensive.
175
+ * Use raycastPhysics for raycasting against physic colliders only. Depending on your scenario this might be faster.
176
+ * @param options raycast options. If null, default options will be used.
160
177
  */
161
178
  public raycast(options: RaycastOptions | null = null): Array<Intersection> {
162
179
  if (!options) options = this.defaultRaycastOptions;
@@ -222,8 +239,12 @@ export class Physics {
222
239
 
223
240
  private rapierRay = new RAPIER.Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 });
224
241
  private raycastVectorsBuffer = new CircularBuffer(() => new Vector3(), 10);
225
-
226
- /** raycast against colliders */
242
+ /** Fast raycast against physics colliders
243
+ * @param origin ray origin in screen or worldspace
244
+ * @param direction ray direction in worldspace
245
+ * @param maxDistance max distance to raycast
246
+ * @param solid if true it will also hit the collider if origin is already inside it
247
+ */
227
248
  public raycastPhysicsFast(origin: Vec2 | Vec3, direction: Vec3 | undefined = undefined, maxDistance: number = Infinity, solid: boolean = true)
228
249
  : null | { point: Vector3, collider: ICollider } {
229
250
 
@@ -285,6 +306,72 @@ export class Physics {
285
306
  return ray;
286
307
  }
287
308
 
309
+
310
+ private rapierSphere: RAPIER.Ball | null = null;
311
+ private rapierColliderArray: Array<SphereOverlapResult> = [];
312
+ private readonly rapierIdentityRotation = { x: 0, y: 0, z: 0, w: 1 };
313
+ private readonly rapierForwardVector = { x: 0, y: 0, z: 1 };
314
+ /** Precice sphere overlap detection using rapier against colliders
315
+ * @param point center of the sphere in worldspace
316
+ * @param radius radius of the sphere
317
+ * @returns array of colliders that overlap with the sphere. Note: they currently only contain the collider and the gameobject
318
+ */
319
+ public sphereOverlapPhysics(point: Vector3, radius: number): Array<SphereOverlapResult> {
320
+ this.rapierColliderArray.length = 0;
321
+ if (!this.world) return this.rapierColliderArray;
322
+ if (!this.rapierSphere)
323
+ this.rapierSphere = new RAPIER.Ball(radius);
324
+ this.rapierSphere.radius = radius;
325
+
326
+ this.world.intersectionsWithShape(point, this.rapierIdentityRotation, this.rapierSphere, col => {
327
+ const collider = col[$componentKey] as ICollider
328
+ // if (collider.gameObject.layers.isEnabled(2)) return true;
329
+ const intersection = new SphereOverlapResult(collider.gameObject, collider);
330
+ this.rapierColliderArray.push(intersection);
331
+ return true; // Return `false` instead if we want to stop searching for other colliders that contain this point.
332
+ }, QueryFilterFlags.EXCLUDE_SENSORS, undefined, undefined, undefined,
333
+ col => {
334
+ const collider = col[$componentKey] as ICollider
335
+ return collider.gameObject.layers.isEnabled(2) == false
336
+ }
337
+ );
338
+ return this.rapierColliderArray;
339
+
340
+
341
+ // TODO: this only returns one hit
342
+ // let filterGroups = 0xffffffff;
343
+ // filterGroups &= ~(1 << 2);
344
+ // const hit: ShapeColliderTOI | null = this.world.castShape(point,
345
+ // this.rapierIdentityRotation,
346
+ // this.rapierForwardVector,
347
+ // this.rapierSphere,
348
+ // 0,
349
+ // QueryFilterFlags.EXCLUDE_SENSORS,
350
+ // // filterGroups,
351
+ // );
352
+ // // console.log(hit);
353
+ // if (hit) {
354
+ // const collider = hit.collider[$componentKey] as ICollider
355
+ // const intersection = new SphereOverlapResult(collider.gameObject);
356
+ // this.rapierColliderArray.push(intersection);
357
+ // // const localpt = hit.witness2;
358
+ // // // const normal = hit.normal2;
359
+ // // const hitPoint = new Vector3(localpt.x, localpt.y, localpt.z);
360
+ // // // collider.gameObject.localToWorld(hitPoint);
361
+ // // // const normalPt = new Vector3(normal.x, normal.y, normal.z);
362
+ // // // const mat = new Matrix4().setPosition(point).scale(new Vector3(radius, radius, radius));
363
+ // // // hitPoint.applyMatrix4(mat);
364
+ // // console.log(hit.witness2)
365
+ // // // hitPoint.add(point);
366
+ // // const dist = hitPoint.distanceTo(point);
367
+ // }
368
+
369
+ // return this.rapierColliderArray;
370
+ }
371
+
372
+
373
+
374
+
288
375
  // physics simulation
289
376
 
290
377
  enabled: boolean = true;
@@ -352,42 +439,58 @@ export class Physics {
352
439
 
353
440
  addBoxCollider(collider: ICollider, center: Vector3, size: Vector3) {
354
441
  if (!this.enabled) {
355
- if(debugPhysics) console.warn("Physics is disabled");
442
+ if (debugPhysics) console.warn("Physics are disabled");
356
443
  return;
357
444
  }
358
445
  const obj = collider.gameObject;
359
446
  const scale = getWorldScale(obj, this._tempPosition).multiply(size);
360
447
  scale.multiplyScalar(0.5);
448
+
449
+ // prevent negative scale
450
+ if (scale.x < 0)
451
+ scale.x = Math.abs(scale.x);
452
+ if (scale.y < 0)
453
+ scale.y = Math.abs(scale.y);
454
+ if (scale.z < 0)
455
+ scale.z = Math.abs(scale.z);
456
+
361
457
  const desc = ColliderDesc.cuboid(scale.x, scale.y, scale.z);
458
+ // const objectLayerMask = collider.gameObject.layers.mask;
459
+ // const mask = objectLayerMask & ~2;
460
+ // desc.setCollisionGroups(objectLayerMask);
362
461
  this.createCollider(collider, desc, center);
363
462
  }
364
463
 
365
464
  addSphereCollider(collider: ICollider, center: Vector3, radius: number) {
366
465
  if (!this.enabled) {
367
- if(debugPhysics) console.warn("Physics is disabled");
466
+ if (debugPhysics) console.warn("Physics are disabled");
368
467
  return;
369
468
  }
370
469
  const obj = collider.gameObject;
371
470
  const scale = getWorldScale(obj, this._tempPosition).multiplyScalar(radius);
471
+ // Prevent negative scales
472
+ scale.x = Math.abs(scale.x);
372
473
  const desc = ColliderDesc.ball(scale.x);
373
474
  this.createCollider(collider, desc, center);
374
475
  }
375
476
 
376
477
  addCapsuleCollider(collider: ICollider, center: Vector3, height: number, radius: number) {
377
478
  if (!this.enabled) {
378
- if(debugPhysics) console.warn("Physics is disabled");
479
+ if (debugPhysics) console.warn("Physics are disabled");
379
480
  return;
380
481
  }
381
482
  const obj = collider.gameObject;
382
483
  const scale = getWorldScale(obj, this._tempPosition);
383
- if (debugPhysics) console.log("capsule scale", scale, height, radius);
484
+ // Prevent negative scales
485
+ scale.x = Math.abs(scale.x);
486
+ scale.y = Math.abs(scale.y);
384
487
  const desc = ColliderDesc.capsule(height * .5 * scale.y - radius, radius * scale.x);
385
488
  this.createCollider(collider, desc, center);
386
489
  }
387
490
 
388
491
  addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale: Vector3) {
389
492
  if (!this.enabled) {
390
- if(debugPhysics) console.warn("Physics is disabled");
493
+ if (debugPhysics) console.warn("Physics are disabled");
391
494
  return;
392
495
  }
393
496
  const geo = mesh.geometry;
@@ -744,6 +744,10 @@ export class Context implements IContext {
744
744
  this.connection.sendBufferedMessagesNow();
745
745
 
746
746
  this._stats?.end();
747
+
748
+ if (this.time.frame === 1) {
749
+ this.domElement.dispatchEvent(new CustomEvent("ready"));
750
+ }
747
751
  }
748
752
 
749
753
  renderNow(camera?: Camera) {
@@ -23,7 +23,10 @@ import {
23
23
  DecrementWrapStencilOp,
24
24
  InvertStencilOp,
25
25
  } from "three";
26
- import { getParam } from "../engine_utils";
26
+ import { getParam, isDebugMode } from "../engine_utils";
27
+ import { showBalloonWarning } from "../debug";
28
+ import { isUsingInstancing } from "../engine_gameobject";
29
+ import { isLocalNetwork } from "../engine_networking_utils";
27
30
 
28
31
  const debug = getParam("debugstencil");
29
32
 
@@ -62,6 +65,12 @@ export class NEEDLE_render_objects implements GLTFLoaderPlugin {
62
65
  const stencil: StencilSettingsModel = settings[i];
63
66
  if (matchesLayer(stencil.layer, obj)) {
64
67
  if (debug) console.log(stencil);
68
+ setTimeout(() => {
69
+ if (isLocalNetwork() && isUsingInstancing(obj.gameObject)) {
70
+ showBalloonWarning("Stencil not supported on instanced objects");
71
+ console.warn("Stencil not supported on instanced objects", obj);
72
+ }
73
+ }, 500)
65
74
  for (let i = 0; i < obj.sharedMaterials.length; i++) {
66
75
  let mat = obj.sharedMaterials[i];
67
76
  if (mat) {
@@ -73,7 +73,6 @@ export class Animator extends Behaviour {
73
73
  /**@deprecated use getBool */
74
74
  GetBool(name: string | number) { return this.getBool(name); }
75
75
  getBool(name: string | number): boolean {
76
- console.log("name", name);
77
76
  return this.runtimeAnimatorController?.getBool(name) ?? false;
78
77
  }
79
78
 
@@ -86,6 +86,9 @@ export class CharacterControllerInput extends Behaviour {
86
86
  @serializable()
87
87
  jumpForce: number = 1;
88
88
 
89
+ @serializable()
90
+ doubleJumpForce: number = 2;
91
+
89
92
  @serializable(Animator)
90
93
  animator?: Animator;
91
94
 
@@ -106,7 +109,7 @@ export class CharacterControllerInput extends Behaviour {
106
109
 
107
110
  if (this.controller?.isGrounded) {
108
111
  this._jumpCount = 0;
109
- this.animator?.SetBool("doubleJump", false);
112
+ if (this.doubleJumpForce > 0) this.animator?.setBool("doubleJump", false);
110
113
  }
111
114
 
112
115
  const forward = this.context.input.isKeyPressed("w");
@@ -121,8 +124,8 @@ export class CharacterControllerInput extends Behaviour {
121
124
  this._currentSpeed.z += step * this.movementSpeed * this.context.time.deltaTime;
122
125
 
123
126
  // if (!this.controller || this.controller.isGrounded)
124
- this.animator?.SetBool("running", step != 0);
125
- this.animator?.SetBool("jumping", this.controller?.isGrounded === true && jump);
127
+ this.animator?.setBool("running", step != 0);
128
+ this.animator?.setBool("jumping", this.controller?.isGrounded === true && jump);
126
129
 
127
130
  this._temp.copy(this._currentSpeed);
128
131
  this._temp.applyQuaternion(this.gameObject.quaternion);
@@ -146,9 +149,9 @@ export class CharacterControllerInput extends Behaviour {
146
149
 
147
150
  if (this.controller && jump && this.jumpForce > 0) {
148
151
  let canJump = this.controller?.isGrounded;
149
- if (!this.controller?.isGrounded && this._jumpCount === 1) {
152
+ if (this.doubleJumpForce > 0 && !this.controller?.isGrounded && this._jumpCount === 1) {
150
153
  canJump = true;
151
- this.animator?.SetBool("doubleJump", true);
154
+ this.animator?.setBool("doubleJump", true);
152
155
  }
153
156
 
154
157
  if (canJump) {
@@ -156,8 +159,8 @@ export class CharacterControllerInput extends Behaviour {
156
159
  // TODO: factor in mass
157
160
  const rb = this.controller.rigidbody;
158
161
  // const fullJumpHoldLength = .1;
159
- const factor = this._jumpCount === 2 ? 2 : 1;// Mathf.clamp((this.context.time.time - this._jumpDownTime), 0, fullJumpHoldLength) / fullJumpHoldLength;
160
- rb.applyImpulse(new Vector3(0, 1, 0).multiplyScalar(this.jumpForce * factor));
162
+ const factor = this._jumpCount === 2 ? this.doubleJumpForce : this.jumpForce;// Mathf.clamp((this.context.time.time - this._jumpDownTime), 0, fullJumpHoldLength) / fullJumpHoldLength;
163
+ rb.applyImpulse(new Vector3(0, 1, 0).multiplyScalar(factor));
161
164
  }
162
165
  }
163
166
 
@@ -174,10 +177,10 @@ export class CharacterControllerInput extends Behaviour {
174
177
  const hits = this.context.physics.raycast(this._raycastOptions);
175
178
  this.gameObject.layers.set(currentLayer);
176
179
  if ((hits.length && hits[0].distance > 2 || verticalSpeed < -10)) {
177
- this.animator?.SetBool("falling", true);
180
+ this.animator?.setBool("falling", true);
178
181
  }
179
182
  }
180
- else this.animator?.SetBool("falling", false);
183
+ else this.animator?.setBool("falling", false);
181
184
  }
182
185
  }
183
186
 
@@ -1,7 +1,7 @@
1
1
  import { Behaviour } from "./Component";
2
2
  import { Rigidbody } from "./RigidBody";
3
3
  import { serializable } from "../engine/engine_serialization_decorator";
4
- import { Event, Mesh, Object3D, Vector3 } from "three"
4
+ import { Event, Group, Mesh, Object3D, Vector3 } from "three"
5
5
  // import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics";
6
6
  import { ICollider } from "../engine/engine_types";
7
7
  import { getWorldScale } from "../engine/engine_three_utils";
@@ -77,6 +77,7 @@ export class MeshCollider extends Collider {
77
77
 
78
78
 
79
79
  sharedMesh?: Mesh;
80
+
80
81
  @serializable()
81
82
  convex: boolean = false;
82
83
 
@@ -88,8 +89,21 @@ export class MeshCollider extends Collider {
88
89
  this.sharedMesh = this.gameObject;
89
90
  }
90
91
  }
91
- if (this.sharedMesh?.isMesh)
92
+ if (this.sharedMesh?.isMesh) {
92
93
  this.context.physics.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
94
+ }
95
+ else {
96
+ const group = this.sharedMesh as any as Group;
97
+ if (group?.isGroup) {
98
+ console.warn(`MeshCollider mesh is a group \"${this.sharedMesh?.name}\", adding all children as colliders. This is currently not fully supported (colliders can not be removed from world again)`, this)
99
+ for (const ch in group.children) {
100
+ const child = group.children[ch] as Mesh;
101
+ if (child.isMesh) {
102
+ this.context.physics.addMeshCollider(this, child, this.convex, getWorldScale(this.gameObject));
103
+ }
104
+ }
105
+ }
106
+ }
93
107
  }
94
108
  }
95
109
 
@@ -330,7 +330,7 @@ class SizeBehaviour extends ParticleSystemBaseBehaviour {
330
330
  }
331
331
  }
332
332
 
333
- const $particleLife = Symbol("particleLife");
333
+ export const $particleLife = Symbol("particleLife");
334
334
  const $trailLifetime = Symbol("trailLifetime");
335
335
  const $trailStartLength = Symbol("trailStartLength");
336
336
 
@@ -340,7 +340,6 @@ class TrailBehaviour extends ParticleSystemBaseBehaviour {
340
340
  initialize(particle: Particle) {
341
341
  if (particle instanceof TrailParticle) {
342
342
  particle[$particleLife] = particle.life;
343
- particle[$trailLifetime] = particle.life;
344
343
  if (this.system.trails.enabled && this.system.trails.dieWithParticles === false) {
345
344
  particle[$trailLifetime] = this.system.trails.lifetime.evaluate(Math.random(), Math.random());
346
345
  particle.life += particle[$trailLifetime];
@@ -418,7 +417,7 @@ class VelocityBehaviour extends ParticleSystemBaseBehaviour {
418
417
  if (gravityFactor !== 0) {
419
418
  // gravityFactor *= -1;
420
419
  temp3.copy(this._gravityDirection).multiplyScalar(gravityFactor);
421
- if(debug) Gizmos.DrawDirection(particle.position, temp3, 0x0000ff, 0, false, 10);
420
+ if (debug) Gizmos.DrawDirection(particle.position, temp3, 0x0000ff, 0, false, 10);
422
421
  baseVelocity.add(temp3);
423
422
  }
424
423
  particle.velocity.copy(baseVelocity);
@@ -747,6 +746,7 @@ export class ParticleSystem extends Behaviour implements IParticleSystem {
747
746
  private _time: number = 0;
748
747
  private _isPlaying: boolean = true;
749
748
  private _isUsedAsSubsystem: boolean = false;
749
+ private _didPreWarm: boolean = false;
750
750
 
751
751
  /** called from deserialization */
752
752
  private set bursts(arr: ParticleBurst[]) {
@@ -779,7 +779,8 @@ export class ParticleSystem extends Behaviour implements IParticleSystem {
779
779
  }
780
780
  private _subEmitterSystems?: SubEmitterSystem[];
781
781
 
782
- onAfterDeserialize(_){
782
+ onAfterDeserialize(_) {
783
+ // doing this here to get a chance to resolve the subemitter guid
783
784
  if (this._subEmitterSystems && Array.isArray(this._subEmitterSystems)) {
784
785
  for (const sub of this._subEmitterSystems) {
785
786
  sub._deserialize(this.context, this.gameObject);
@@ -788,7 +789,6 @@ export class ParticleSystem extends Behaviour implements IParticleSystem {
788
789
  }
789
790
 
790
791
  awake(): void {
791
-
792
792
  this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as ParticleSystemRenderer;
793
793
 
794
794
  this._container = new Object3D();
@@ -837,16 +837,42 @@ export class ParticleSystem extends Behaviour implements IParticleSystem {
837
837
  onEnable() {
838
838
  if (this.inheritVelocity)
839
839
  this.inheritVelocity.system = this;
840
+ if (this._batchSystem)
841
+ this._batchSystem.visible = true;
840
842
  this.play();
841
843
  }
842
844
 
843
845
  onDisable() {
846
+ if (this._batchSystem)
847
+ this._batchSystem.visible = false;
844
848
  }
845
849
 
846
850
  onBeforeRender() {
851
+ if (this._didPreWarm === false && this.main?.prewarm === true) {
852
+ this._didPreWarm = true;
853
+ this.preWarm();
854
+ }
847
855
  this.onUpdate();
848
- const dt = this.deltaTime;
856
+ this.onSimulate(this.deltaTime);
857
+ }
858
+
859
+ private preWarm() {
860
+ const dt = 1 / 60;
861
+ const duration = this.main.duration;
862
+ const lifetime = this.main.startLifetime.getMax();
863
+ const maxDurationToPrewarm = 1000;
864
+ const timeToSimulate = Math.min(duration, lifetime, maxDurationToPrewarm);
865
+ const framesToSimulate = Math.ceil(timeToSimulate / dt);
866
+ if (debug)
867
+ console.log(`Particles ${this.name} - Prewarm for ${framesToSimulate} frames (${timeToSimulate} sec). Duration: ${duration}, Lifetime: ${lifetime}`);
868
+ for (let i = 0; i < framesToSimulate; i++) {
869
+ if (this.currentParticles >= this.maxParticles) break;
870
+ this.onUpdate();
871
+ this.onSimulate(dt);
872
+ }
873
+ }
849
874
 
875
+ private onSimulate(dt: number) {
850
876
  if (this._batchSystem) {
851
877
  // Updating layers on batches
852
878
  // TODO: figure out a better way to do this
@@ -942,10 +968,20 @@ export class SubEmitterSystem {
942
968
  properties?: number;
943
969
  type?: SubEmitterType;
944
970
 
945
- _deserialize(context: Context, _gameObject: GameObject) {
971
+ _deserialize(_context: Context, gameObject: GameObject) {
946
972
  const ps = this.particleSystem;
947
- if (ps && !(ps instanceof ParticleSystem) && typeof ps["guid"] === "string") {
948
- this.particleSystem = GameObject.findByGuid(ps["guid"], context.scene) as ParticleSystem;
973
+ if (ps instanceof ParticleSystem) return;
974
+
975
+ let guid = "";
976
+
977
+ if (ps && typeof ps["guid"] === "string") {
978
+ guid = ps["guid"];
979
+ // subemitter MUST be a child of the particle system
980
+ this.particleSystem = GameObject.findByGuid(guid, gameObject) as ParticleSystem;
981
+ }
982
+
983
+ if (debug && !(this.particleSystem instanceof ParticleSystem)) {
984
+ console.warn("Could not find particle system for sub emitter", guid, gameObject, this);
949
985
  }
950
986
  }
951
987
  }
@@ -199,6 +199,33 @@ export class MinMaxCurve {
199
199
  }
200
200
  return 0;
201
201
  }
202
+
203
+ getMax(): number {
204
+ switch (this.mode) {
205
+ case ParticleSystemCurveMode.Constant:
206
+ return this.constant;
207
+ case ParticleSystemCurveMode.Curve:
208
+ return this.getMaxFromCurve(this.curve!) * this.curveMultiplier!;
209
+ case ParticleSystemCurveMode.TwoCurves:
210
+ return Math.max(this.getMaxFromCurve(this.curveMin), this.getMaxFromCurve(this.curveMax)) * this.curveMultiplier!;
211
+ case ParticleSystemCurveMode.TwoConstants:
212
+ return Math.max(this.constantMin, this.constantMax);
213
+ default:
214
+ return 0;
215
+ }
216
+ }
217
+
218
+ private getMaxFromCurve(curve?: AnimationCurve) {
219
+ if (!curve) return 0;
220
+ let maxNumber = Number.MIN_VALUE;
221
+ for (let i = 0; i < curve!.keys.length; i++) {
222
+ const key = curve!.keys[i];
223
+ if (key.value > maxNumber) {
224
+ maxNumber = key.value;
225
+ }
226
+ }
227
+ return maxNumber;
228
+ }
202
229
  }
203
230
 
204
231
  export class MinMaxGradient {
@@ -571,7 +598,7 @@ export class ShapeModule implements EmitterShape {
571
598
  if (this.enabled) {
572
599
  switch (this.shapeType) {
573
600
  case ParticleSystemShapeType.Box:
574
- if(debug) Gizmos.DrawBox(this.position, this.scale, 0xdddddd, 1);
601
+ if (debug) Gizmos.DrawBox(this.position, this.scale, 0xdddddd, 1);
575
602
  this._vector.x = Math.random() * this.scale.x - this.scale.x / 2;
576
603
  this._vector.y = Math.random() * this.scale.y - this.scale.y / 2;
577
604
  this._vector.z = Math.random() * this.scale.z - this.scale.z / 2;
@@ -2,7 +2,7 @@ import { Behavior, Particle, EmissionState, ParticleSystem, ParticleEmitter } fr
2
2
  import { Vector3, Quaternion, Matrix4 } from "three";
3
3
  import { IParticleSystem } from "./ParticleSystemModules";
4
4
  import { CircularBuffer } from "../engine/engine_utils";
5
- import { SubEmitterType } from "./ParticleSystem";
5
+ import { $particleLife, SubEmitterType } from "./ParticleSystem";
6
6
 
7
7
  const VECTOR_ONE = new Vector3(1, 1, 1);
8
8
  const VECTOR_Z = new Vector3(0, 0, 1);
@@ -73,13 +73,15 @@ export class ParticleSubEmitter implements Behavior {
73
73
  if (!this.subParticleSystem || !particle.emissionState)
74
74
  return;
75
75
 
76
- if(this.emitterProbability && Math.random() > this.emitterProbability)
76
+ if (this.emitterProbability && Math.random() > this.emitterProbability)
77
77
  return;
78
78
 
79
79
  const delta = this.system.deltaTime;
80
80
 
81
81
  if (this.emitterType === SubEmitterType.Death) {
82
- const willDie = particle.age + delta * 1.2 >= particle.life;
82
+ let lifeTime = particle.life;
83
+ if (particle[$particleLife] !== undefined) lifeTime = particle[$particleLife];
84
+ const willDie = particle.age + delta * 1.2 >= lifeTime;
83
85
  if (!willDie) return;
84
86
  // Just emit all for now, we should probably add a way to get the amount from the subsystem emission module
85
87
  const maxAmount = this.subSystem.main.maxParticles - this.subSystem.currentParticles;