@needle-tools/engine 4.3.0 → 4.3.2-beta

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 (52) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/components.needle.json +1 -1
  3. package/dist/needle-engine.bundle.js +3803 -3770
  4. package/dist/needle-engine.bundle.light.js +3805 -3775
  5. package/dist/needle-engine.bundle.light.min.js +125 -112
  6. package/dist/needle-engine.bundle.light.umd.cjs +113 -100
  7. package/dist/needle-engine.bundle.min.js +125 -112
  8. package/dist/needle-engine.bundle.umd.cjs +113 -100
  9. package/dist/needle-engine.d.ts +9 -9
  10. package/dist/needle-engine.js +509 -507
  11. package/dist/needle-engine.light.d.ts +9 -9
  12. package/dist/needle-engine.light.js +509 -507
  13. package/dist/needle-engine.light.min.js +1 -1
  14. package/dist/needle-engine.light.umd.cjs +1 -1
  15. package/dist/needle-engine.min.js +1 -1
  16. package/dist/needle-engine.umd.cjs +1 -1
  17. package/lib/engine/engine_time.d.ts +11 -2
  18. package/lib/engine/engine_time.js +12 -1
  19. package/lib/engine/engine_time.js.map +1 -1
  20. package/lib/engine-components/Duplicatable.d.ts +2 -6
  21. package/lib/engine-components/Duplicatable.js +21 -20
  22. package/lib/engine-components/Duplicatable.js.map +1 -1
  23. package/lib/engine-components/OrbitControls.d.ts +1 -0
  24. package/lib/engine-components/OrbitControls.js +8 -0
  25. package/lib/engine-components/OrbitControls.js.map +1 -1
  26. package/lib/engine-components/Renderer.js +12 -6
  27. package/lib/engine-components/Renderer.js.map +1 -1
  28. package/lib/engine-components/RendererInstancing.d.ts +1 -3
  29. package/lib/engine-components/RendererInstancing.js +31 -63
  30. package/lib/engine-components/RendererInstancing.js.map +1 -1
  31. package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +1 -1
  32. package/lib/engine-components/postprocessing/PostProcessingHandler.js +6 -14
  33. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  34. package/lib/engine-components/postprocessing/Volume.d.ts +15 -0
  35. package/lib/engine-components/postprocessing/Volume.js +73 -4
  36. package/lib/engine-components/postprocessing/Volume.js.map +1 -1
  37. package/lib/engine-components/ui/Canvas.js +2 -2
  38. package/lib/engine-components/ui/Canvas.js.map +1 -1
  39. package/lib/needle-engine.js +8 -6
  40. package/lib/needle-engine.js.map +1 -1
  41. package/package.json +1 -1
  42. package/plugins/common/buildinfo.js +8 -3
  43. package/plugins/vite/copyfiles.js +1 -1
  44. package/src/engine/engine_time.ts +14 -2
  45. package/src/engine-components/Duplicatable.ts +21 -19
  46. package/src/engine-components/OrbitControls.ts +8 -0
  47. package/src/engine-components/Renderer.ts +12 -5
  48. package/src/engine-components/RendererInstancing.ts +32 -68
  49. package/src/engine-components/postprocessing/PostProcessingHandler.ts +8 -14
  50. package/src/engine-components/postprocessing/Volume.ts +82 -6
  51. package/src/engine-components/ui/Canvas.ts +2 -2
  52. package/src/needle-engine.ts +8 -6
@@ -65,7 +65,7 @@ async function run(buildstep, config) {
65
65
 
66
66
  const outDir = resolve(baseDir, outdirName);
67
67
  if (!existsSync(outDir)) {
68
- mkdirSync(outDir);
68
+ mkdirSync(outDir, { recursive: true });
69
69
  }
70
70
 
71
71
  // copy a list of files or directories declared in build.copy = [] in the needle.config.json
@@ -42,7 +42,19 @@ export class Time implements ITime {
42
42
  return this.clock.elapsedTime;
43
43
  }
44
44
 
45
- /** Approximated frames per second (Read Only). */
45
+ /**
46
+ * @returns {Number} FPS for this frame.
47
+ * Note that this returns the raw value (e.g. 59.88023952362959) and will fluctuate a lot between frames.
48
+ * If you want a more stable FPS, use `smoothedFps` instead.
49
+ */
50
+ get fps() {
51
+ return 1 / this.deltaTime;
52
+ }
53
+
54
+ /**
55
+ * Approximated frames per second
56
+ * @returns the smoothed FPS value over the last 60 frames with decimals.
57
+ */
46
58
  get smoothedFps() { return this._smoothedFps; }
47
59
  /** The smoothed time in seconds it took to complete the last frame (Read Only). */
48
60
  get smoothedDeltaTime() { return 1 / this._smoothedFps; }
@@ -51,7 +63,7 @@ export class Time implements ITime {
51
63
  private clock = new Clock();
52
64
  private _smoothedFps: number = 0;
53
65
  private _smoothedDeltaTime: number = 0;
54
- private _fpsSamples: number[] = [];
66
+ private readonly _fpsSamples: number[] = [];
55
67
  private _fpsSampleIndex: number = 0;
56
68
 
57
69
  constructor() {
@@ -1,6 +1,7 @@
1
1
  import { Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment } from "../engine/debug/index.js";
4
+ import { WaitForSeconds } from "../engine/engine_coroutine.js";
4
5
  import { InstantiateOptions } from "../engine/engine_gameobject.js";
5
6
  import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
6
7
  import { serializable } from "../engine/engine_serialization_decorator.js";
@@ -30,17 +31,10 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
30
31
 
31
32
  /**
32
33
  * The maximum number of objects that can be duplicated in the interval.
33
- * @default 10
34
- */
35
- @serializable()
36
- limitCount = 10;
37
-
38
- /**
39
- * The interval in seconds in which the limitCount is reset.
40
34
  * @default 60
41
35
  */
42
36
  @serializable()
43
- limitInterval = 60;
37
+ limitCount = 60;
44
38
 
45
39
  private _currentCount = 0;
46
40
  private _startPosition: Vector3 | null = null;
@@ -94,8 +88,9 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
94
88
 
95
89
  if (!this.gameObject.getComponentInParent(ObjectRaycaster))
96
90
  this.gameObject.addComponent(ObjectRaycaster);
97
-
98
- this.cloneLimitIntervalFn();
91
+ }
92
+ onEnable(): void {
93
+ this.startCoroutine(this.cloneLimitIntervalFn());
99
94
  }
100
95
 
101
96
  private _forwardPointerEvents: Map<Object3D, DragControls> = new Map();
@@ -131,7 +126,12 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
131
126
  }
132
127
  }
133
128
  else {
134
- console.warn("Could not duplicate object. Has the target object been destroyed?", this);
129
+ if (this._currentCount >= this.limitCount) {
130
+ console.warn(`[Duplicatable] Limit of ${this.limitCount} objects created within a few seconds reached. Please wait a moment before creating more objects.`);
131
+ }
132
+ else {
133
+ console.warn(`[Duplicatable] Could not duplicate object.`);
134
+ }
135
135
  }
136
136
  }
137
137
 
@@ -145,19 +145,21 @@ export class Duplicatable extends Behaviour implements IPointerEventHandler {
145
145
  }
146
146
  }
147
147
 
148
- private cloneLimitIntervalFn() {
149
- if (this.destroyed) return;
150
- if (this._currentCount > 0) {
151
- this._currentCount -= 1;
148
+ private *cloneLimitIntervalFn() {
149
+ while (this.activeAndEnabled && !this.destroyed) {
150
+ if (this._currentCount > 0) {
151
+ this._currentCount -= 1;
152
+ }
153
+ else if (this._currentCount < 0) {
154
+ this._currentCount = 0;
155
+ }
156
+ yield WaitForSeconds(1);
152
157
  }
153
- setTimeout(() => {
154
- this.cloneLimitIntervalFn();
155
- }, (this.limitInterval / this.limitCount) * 1000);
156
158
  }
157
159
 
158
160
  private handleDuplication(): Object3D | null {
159
161
  if (!this.object) return null;
160
- if (this._currentCount >= this.limitCount) return null;
162
+ if (this.limitCount > 0 && this._currentCount >= this.limitCount) return null;
161
163
  if (this.object === this.gameObject) return null;
162
164
  if (GameObject.isDestroyed(this.object)) {
163
165
  this.object = null;
@@ -332,6 +332,7 @@ export class OrbitControls extends Behaviour implements ICameraController {
332
332
 
333
333
  this._activePointerEvents = [];
334
334
  this.context.input.addEventListener("pointerdown", this._onPointerDown, { queue: InputEventQueue.Early });
335
+ this.context.input.addEventListener("pointerdown", this._onPointerDownLate, { queue: InputEventQueue.Late });
335
336
  this.context.input.addEventListener("pointerup", this._onPointerUp, { queue: InputEventQueue.Early });
336
337
  this.context.input.addEventListener("pointerup", this._onPointerUpLate, { queue: InputEventQueue.Late });
337
338
  }
@@ -352,6 +353,7 @@ export class OrbitControls extends Behaviour implements ICameraController {
352
353
  }
353
354
  this._activePointerEvents.length = 0;
354
355
  this.context.input.removeEventListener("pointerdown", this._onPointerDown);
356
+ this.context.input.removeEventListener("pointerdown", this._onPointerDownLate);
355
357
  this.context.input.removeEventListener("pointerup", this._onPointerUp);
356
358
  this.context.input.removeEventListener("pointerup", this._onPointerUpLate);
357
359
  }
@@ -363,6 +365,12 @@ export class OrbitControls extends Behaviour implements ICameraController {
363
365
  private _onPointerDown = (_evt: NEPointerEvent) => {
364
366
  this._activePointerEvents.push(_evt);
365
367
  }
368
+ private _onPointerDownLate = (evt: NEPointerEvent) => {
369
+ if(evt.used && this._controls) {
370
+ // Disabling orbit controls here because otherwise we get a slight movement when e.g. using DragControls
371
+ this._controls.enabled = false;
372
+ }
373
+ }
366
374
 
367
375
  private _onPointerUp = (evt: NEPointerEvent) => {
368
376
  // make sure we cleanup the active pointer events
@@ -847,11 +847,18 @@ export class SkinnedMeshRenderer extends MeshRenderer {
847
847
  for (const mesh of this.sharedMeshes) {
848
848
  if (mesh instanceof SkinnedMesh) {
849
849
  this._needUpdateBoundingSphere = false;
850
- const geometry = mesh.geometry;
851
- const raycastmesh = getRaycastMesh(mesh);
852
- if (raycastmesh) mesh.geometry = raycastmesh;
853
- mesh.computeBoundingSphere();
854
- mesh.geometry = geometry;
850
+ try {
851
+ const geometry = mesh.geometry;
852
+ const raycastmesh = getRaycastMesh(mesh);
853
+ if (raycastmesh) {
854
+ mesh.geometry = raycastmesh;
855
+ }
856
+ mesh.computeBoundingSphere();
857
+ mesh.geometry = geometry;
858
+ }
859
+ catch(err) {
860
+ console.error(`Error updating bounding sphere for ${mesh.name}`, err);
861
+ }
855
862
  }
856
863
  }
857
864
  }
@@ -1,6 +1,7 @@
1
1
  import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial } from "three";
2
2
 
3
3
  import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js";
4
+ import { Gizmos } from "../engine/engine_gizmos.js";
4
5
  import { $instancingAutoUpdateBounds, $instancingRenderer, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
5
6
  import { Context } from "../engine/engine_setup.js";
6
7
  import { getParam, makeIdFromRandomWords } from "../engine/engine_utils.js";
@@ -337,6 +338,12 @@ class InstancedMeshRenderer {
337
338
  this._batchedMesh.computeBoundingBox();
338
339
  if (sphere)
339
340
  this._batchedMesh.computeBoundingSphere();
341
+ if (debugInstancing && this._batchedMesh.boundingSphere) {
342
+ const sphere = this._batchedMesh.boundingSphere;
343
+ // const worldPos = this._batchedMesh.worldPosition.add(sphere.center);
344
+ // const worldRadius = sphere!.radius;
345
+ Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0x00ff00);
346
+ }
340
347
  }
341
348
 
342
349
  private _context: Context;
@@ -401,7 +408,7 @@ class InstancedMeshRenderer {
401
408
  // break;
402
409
  // }
403
410
  // }
404
- if(!canMergeMaterial) {
411
+ if (!canMergeMaterial) {
405
412
  return false;
406
413
  }
407
414
  }
@@ -566,7 +573,7 @@ class InstancedMeshRenderer {
566
573
  this.markNeedsUpdate();
567
574
  }
568
575
 
569
- updateGeometry(geo: BufferGeometry, index: number) {
576
+ updateGeometry(geo: BufferGeometry, geometryIndex: number): boolean {
570
577
  if (!this.validateGeometry(geo)) {
571
578
  return false;
572
579
  }
@@ -575,10 +582,11 @@ class InstancedMeshRenderer {
575
582
  this.grow(geo);
576
583
  }
577
584
  if (debugInstancing)
578
- console.debug("UPDATE MESH", index, this._batchedMesh["_geometryCount"], geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0);
579
-
585
+ console.debug("[Instancing] UPDATE GEOMETRY at " + geometryIndex, this._batchedMesh["_geometryCount"], geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0);
580
586
 
581
- this._batchedMesh.setGeometryAt(index, geo);
587
+ this._batchedMesh.setGeometryAt(geometryIndex, geo);
588
+ // for LOD mesh updates we need to make sure to save the geometry index
589
+ this._geometryIds.set(geo, geometryIndex);
582
590
  this.markNeedsUpdate();
583
591
  return true;
584
592
  }
@@ -588,7 +596,7 @@ class InstancedMeshRenderer {
588
596
  this._batchedMesh.layers.enableAll();
589
597
 
590
598
  if (this._needUpdateBounds && this._batchedMesh[$instancingAutoUpdateBounds] === true) {
591
- if (debugInstancing) console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate);
599
+ if (debugInstancing === "verbose") console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate);
592
600
  this.updateBounds();
593
601
  }
594
602
  }
@@ -623,7 +631,9 @@ class InstancedMeshRenderer {
623
631
  }
624
632
 
625
633
  private markNeedsUpdate() {
626
- if (debugInstancing) console.warn("Marking instanced mesh dirty", this.name);
634
+ if (debugInstancing === "verbose") {
635
+ console.warn("Marking instanced mesh dirty", this.name);
636
+ }
627
637
  this._needUpdateBounds = true;
628
638
  // this.inst.instanceMatrix.needsUpdate = true;
629
639
  }
@@ -641,19 +651,23 @@ class InstancedMeshRenderer {
641
651
  }
642
652
 
643
653
  private grow(geometry: BufferGeometry) {
644
- const newSize = this._maxInstanceCount * 2;
654
+ const growFactor = 2;
655
+ const newSize = Math.ceil(this._maxInstanceCount * growFactor);
645
656
 
646
657
  // create a new BatchedMesh instance
647
658
  const estimatedSpace = this.tryEstimateVertexCountSize(newSize, [geometry]);// geometry.attributes.position.count;
648
659
  // const indices = geometry.index ? geometry.index.count : 0;
649
660
  const newMaxVertexCount = Math.max(this._maxVertexCount, estimatedSpace.vertexCount);
650
- const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, this._maxVertexCount * 2);
661
+ const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, Math.ceil(this._maxVertexCount * growFactor));
651
662
 
652
663
  if (debugInstancing) {
653
664
  const geometryInfo = getMeshInformation(geometry);
654
- console.warn(`Growing batched mesh for \"${this.name}/${geometry.name}\" ${geometryInfo.vertexCount} vertices, ${geometryInfo.indexCount} indices\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
665
+ console.warn(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\n${geometryInfo.vertexCount} vertices, ${geometryInfo.indexCount} indices\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
655
666
  this._debugMaterial = createDebugMaterial();
656
667
  }
668
+ else if (isDevEnvironment()) {
669
+ console.debug(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`);
670
+ }
657
671
 
658
672
  this._maxVertexCount = newMaxVertexCount;
659
673
  this._maxIndexCount = newMaxIndexCount;
@@ -679,8 +693,6 @@ class InstancedMeshRenderer {
679
693
 
680
694
  // since we have a new batched mesh we need to re-add all the instances
681
695
  // fixes https://linear.app/needle/issue/NE-5711
682
- this._usedBuckets.length = 0;
683
- this._availableBuckets.length = 0;
684
696
 
685
697
  // add current instances to new instanced mesh
686
698
  const original = [...this._handles];
@@ -745,70 +757,26 @@ class InstancedMeshRenderer {
745
757
  }
746
758
 
747
759
 
748
- private readonly _availableBuckets = new Array<BucketInfo>();
749
- private readonly _usedBuckets = new Array<BucketInfo>();
750
-
751
760
  private addGeometry(handle: InstanceHandle) {
752
761
 
753
- const geo = handle.object.geometry as BufferGeometry;
762
+ const obj = handle.object;
763
+ const geo = obj.geometry as BufferGeometry;
754
764
  if (!geo) {
755
765
  // if the geometry is null we cannot add it
756
766
  return;
757
767
  }
758
768
 
759
- // if (handle.reservedVertexCount <= 0 || handle.reservedIndexCount <= 0) {
760
- // console.error("Cannot add geometry with 0 vertices or indices", handle.name);
761
- // return;
762
- // }
763
- // search the smallest available bucket that fits our handle
764
- let smallestBucket: BucketInfo | null = null;
765
- let smallestBucketIndex = -1;
766
- for (let i = this._availableBuckets.length - 1; i >= 0; i--) {
767
- const bucket = this._availableBuckets[i];
768
- if (bucket.vertexCount >= handle.maxVertexCount && bucket.indexCount >= handle.maxIndexCount) {
769
- if (smallestBucket == null || bucket.vertexCount < smallestBucket.vertexCount) {
770
- smallestBucket = bucket;
771
- smallestBucketIndex = i;
772
- }
773
- }
774
- }
775
- // if we have a bucket that is big enough, use it
776
- if (smallestBucket != null) {
777
- const bucket = smallestBucket;
778
- if (debugInstancing)
779
- console.debug(`RE-USE SPACE #${bucket.geometryIndex}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name}`);
780
- try {
781
- this._batchedMesh.setGeometryAt(bucket.geometryIndex, handle.object.geometry as BufferGeometry);
782
- const newIndex = this._batchedMesh.addInstance(bucket.geometryIndex);
783
- this._batchedMesh.setMatrixAt(newIndex, handle.object.matrixWorld);
784
- this._batchedMesh.setVisibleAt(newIndex, true);
785
- handle.__instanceIndex = newIndex;
786
- this._usedBuckets[bucket.geometryIndex] = bucket;
787
- this._availableBuckets.splice(smallestBucketIndex, 1);
788
- return;
789
- }
790
- catch (err) {
791
- if (debugInstancing)
792
- console.error("Failed to re-use space", err);
793
- else if (isDevEnvironment()) {
794
- console.warn(`Failed to re-use space \"${err instanceof Error ? err.message : err}\" in bucket ${bucket.geometryIndex} (${bucket.vertexCount}) - will add new geometry instead`);
795
- }
796
- }
797
- }
798
-
799
769
  // otherwise add more geometry / instances
800
770
  let geometryId = this._geometryIds.get(geo);
801
771
  if (geometryId === undefined || geometryId === null) {
802
772
  if (debugInstancing)
803
- console.debug("ADD GEOMETRY & INSTANCE", geo.name, "\nvertex:", `${this._currentVertexCount} + ${handle.maxVertexCount} < ${this._maxVertexCount}?`, "\nindex:", handle.maxIndexCount, this._currentIndexCount, this._maxIndexCount);
804
-
805
-
773
+ console.debug(`[Instancing] > ADD NEW GEOMETRY \"${handle.name} (${geo.name}; ${geo.uuid})\"\n${this._currentInstanceCount} instances, ${handle.maxVertexCount} max vertices, ${handle.maxIndexCount} max indices`);
806
774
 
807
775
  geometryId = this._batchedMesh.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount);
808
776
  this._geometryIds.set(geo, geometryId);
809
777
  }
810
778
  else {
811
- if (debugInstancing) console.log("ADD INSTANCE", geometryId, geo.name);
779
+ if (debugInstancing === "verbose") console.log(`[Instancing] > ADD INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances`);
812
780
  }
813
781
  this._currentVertexCount += handle.maxVertexCount;
814
782
  this._currentIndexCount += handle.maxIndexCount;
@@ -817,10 +785,9 @@ class InstancedMeshRenderer {
817
785
  handle.__instanceIndex = i;
818
786
  handle.__reservedVertexRange = handle.maxVertexCount;
819
787
  handle.__reservedIndexRange = handle.maxIndexCount;
820
- this._usedBuckets[i] = { geometryIndex: geometryId, vertexCount: handle.maxVertexCount, indexCount: handle.maxIndexCount };
821
788
  this._batchedMesh.setMatrixAt(i, handle.object.matrixWorld);
822
789
  if (debugInstancing)
823
- console.debug(`ADD MESH & RESERVE SPACE #${i}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name} ${handle.object.uuid}`);
790
+ console.debug(`[Instancing] > ADDED INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
824
791
 
825
792
  }
826
793
 
@@ -830,19 +797,16 @@ class InstancedMeshRenderer {
830
797
  console.warn("Cannot remove geometry, instance index is invalid", handle.name);
831
798
  return;
832
799
  }
833
- this._usedBuckets.splice(handle.__instanceIndex, 1);
834
800
  // deleteGeometry is currently not useable since there's no optimize method
835
801
  // https://github.com/mrdoob/three.js/issues/27985
836
802
  // if (del)
837
803
  // this.inst.deleteGeometry(handle.__instanceIndex);
838
804
  // else
839
805
  // this._batchedMesh.setVisibleAt(handle.__instanceIndex, false);
806
+ if(debugInstancing) {
807
+ console.debug(`[Instancing] < REMOVE INSTANCE \"${handle.name}\" at [${handle.__instanceIndex}]\nGEOMETRY_ID=${handle.__geometryIndex}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
808
+ }
840
809
  this._batchedMesh.deleteInstance(handle.__instanceIndex);
841
- this._availableBuckets.push({
842
- geometryIndex: handle.__geometryIndex,
843
- vertexCount: handle.reservedVertexCount,
844
- indexCount: handle.reservedIndexCount
845
- });
846
810
  }
847
811
  }
848
812
 
@@ -43,20 +43,20 @@ export class PostProcessingHandler {
43
43
  this.context = context;
44
44
  }
45
45
 
46
- apply(components: PostProcessingEffect[]) {
46
+ apply(components: PostProcessingEffect[]) : Promise<void> {
47
47
  if ("env" in import.meta && import.meta.env.VITE_NEEDLE_USE_POSTPROCESSING === "false") {
48
48
  if (debug) console.warn("Postprocessing is disabled via vite env setting");
49
49
  else console.debug("Postprocessing is disabled via vite env setting");
50
- return;
50
+ return Promise.resolve();
51
51
  }
52
52
  if (!NEEDLE_USE_POSTPROCESSING) {
53
53
  if (debug) console.warn("Postprocessing is disabled via global vite define setting");
54
54
  else console.debug("Postprocessing is disabled via vite define");
55
- return;
55
+ return Promise.resolve();
56
56
  }
57
57
 
58
58
  this._isActive = true;
59
- this.onApply(this.context, components);
59
+ return this.onApply(this.context, components);
60
60
  }
61
61
 
62
62
  unapply() {
@@ -98,11 +98,12 @@ export class PostProcessingHandler {
98
98
 
99
99
  // IMPORTANT
100
100
  // Load postprocessing modules ONLY here to get lazy loading of the postprocessing package
101
- const modules = await Promise.all([
101
+ await Promise.all([
102
102
  MODULES.POSTPROCESSING.load(),
103
103
  MODULES.POSTPROCESSING_AO.load(),
104
104
  // import("./Effects/Sharpening.effect")
105
105
  ]);
106
+
106
107
  // try {
107
108
  // internal_SetSharpeningEffectModule(modules[2]);
108
109
  // }
@@ -179,16 +180,15 @@ export class PostProcessingHandler {
179
180
  // https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12
180
181
  renderer[autoclearSetting] = renderer.autoClear;
181
182
 
182
- const maxSamples = renderer.capabilities.maxSamples;
183
183
  // create composer and set active on context
184
184
  if (!this._composer) {
185
185
  // const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType });
186
186
  this._composer = new MODULES.POSTPROCESSING.MODULE.EffectComposer(renderer, {
187
187
  frameBufferType: HalfFloatType,
188
188
  stencilBuffer: true,
189
- multisampling: Math.min(DeviceUtilities.isMobileDevice() ? 4 : 8, maxSamples),
190
189
  });
191
190
  }
191
+
192
192
  if (context.composer && context.composer !== this._composer) {
193
193
  console.warn("There's already an active EffectComposer in your scene: replacing it with a new one. This might cause unexpected behaviour. Make sure to only use one PostprocessingManager/Volume in your scene.");
194
194
  }
@@ -219,18 +219,12 @@ export class PostProcessingHandler {
219
219
  if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect)
220
220
  effects.push(ef as Effect);
221
221
  else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
222
- // const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
223
- // pass.mainScene = scene;
224
- // pass.name = effects.map(e => e.constructor.name).join(", ");
225
- // pass.enabled = true;
226
- // composer.addPass(pass);
227
- // effects.length = 0;
228
222
  composer.addPass(ef as Pass);
229
223
  }
230
224
  else {
231
225
  // seems some effects are not correctly typed, but three can deal with them,
232
226
  // so we might need to just pass them through
233
- // composer.addPass(ef);
227
+ composer.addPass(ef);
234
228
  }
235
229
  }
236
230
 
@@ -1,9 +1,10 @@
1
1
  import type { Effect } from "postprocessing";
2
2
 
3
3
  import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
4
+ import { Context } from "../../engine/engine_context.js";
4
5
  import type { EditorModification, IEditorModification as IEditorModificationReceiver } from "../../engine/engine_editor-sync.js";
5
6
  import { serializeable } from "../../engine/engine_serialization_decorator.js";
6
- import { getParam } from "../../engine/engine_utils.js";
7
+ import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
7
8
  import { Behaviour } from "../Component.js";
8
9
  import { EffectWrapper } from "./Effects/EffectWrapper.js";
9
10
  import { PostProcessingEffect } from "./PostProcessingEffect.js";
@@ -58,6 +59,16 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
58
59
  @serializeable(VolumeProfile)
59
60
  sharedProfile?: VolumeProfile;
60
61
 
62
+ /**
63
+ * Set multisampling to "auto" to automatically adjust the multisampling level based on performance.
64
+ * Set to a number to manually set the multisampling level.
65
+ * @default "auto"
66
+ * @min 0
67
+ * @max renderer.capabilities.maxSamples
68
+ */
69
+ @serializeable()
70
+ multisampling: "auto" | number = "auto";
71
+
61
72
  /**
62
73
  * Add a post processing effect to the stack and schedules the effect stack to be re-created.
63
74
  */
@@ -66,12 +77,15 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
66
77
  if (!(entry instanceof PostProcessingEffect)) {
67
78
  entry = new EffectWrapper(entry);
68
79
  }
69
- if(entry.gameObject === undefined) this.gameObject.addComponent(entry);
80
+ if (entry.gameObject === undefined) this.gameObject.addComponent(entry);
70
81
  if (this._effects.includes(entry)) return effect;
71
82
  this._effects.push(entry);
72
83
  this._isDirty = true;
73
84
  return effect;
74
85
  }
86
+ /**
87
+ * Remove a post processing effect from the stack and schedules the effect stack to be re-created.
88
+ */
75
89
  removeEffect<T extends PostProcessingEffect | Effect>(effect: T): T {
76
90
 
77
91
  let index = -1;
@@ -106,7 +120,7 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
106
120
  /**
107
121
  * When dirty the post processing effects will be re-applied
108
122
  */
109
- markDirty() {
123
+ markDirty(): void {
110
124
  this._isDirty = true;
111
125
  }
112
126
 
@@ -128,7 +142,13 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
128
142
  this.sharedProfile?.__init(this);
129
143
  }
130
144
 
145
+ private _componentEnabledTime: number = -1;
146
+ private _multisampleAutoChangeTime: number = 0;
147
+ private _multisampleAutoDecreaseTime: number = 0;
148
+
149
+ /** @internal */
131
150
  onEnable(): void {
151
+ this._componentEnabledTime = this.context.time.realtimeSinceStartup;
132
152
  this._isDirty = true;
133
153
  }
134
154
 
@@ -155,7 +175,7 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
155
175
  }
156
176
  }
157
177
 
158
- if (this.context.composer && this._postprocessing?.composer === this.context.composer) {
178
+ if (this.context.composer && this._postprocessing && this._postprocessing.composer === this.context.composer) {
159
179
  if (this.context.renderer.getContext().isContextLost()) {
160
180
  this.context.renderer.forceContextRestore();
161
181
  }
@@ -164,6 +184,40 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
164
184
 
165
185
  this.context.composer.setMainScene(this.context.scene);
166
186
 
187
+ const composer = this.context.composer;
188
+ if (this.multisampling === "auto") {
189
+
190
+ const timeSinceLastChange = this.context.time.realtimeSinceStartup - this._multisampleAutoChangeTime;
191
+
192
+ if (this.context.time.realtimeSinceStartup - this._componentEnabledTime > 2
193
+ && timeSinceLastChange > .5
194
+ ) {
195
+ const prev = composer.multisampling;
196
+
197
+ if (composer.multisampling > 0 && this.context.time.smoothedFps <= 50) {
198
+ this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup;
199
+ this._multisampleAutoDecreaseTime = this.context.time.realtimeSinceStartup;
200
+ composer.multisampling *= .5;
201
+ composer.multisampling = Math.floor(composer.multisampling);
202
+ if (debug) console.debug(`[PostProcessing] Reduced multisampling from ${prev} to ${composer.multisampling}`);
203
+ }
204
+ // if performance is good for a while try increasing multisampling again
205
+ else if (timeSinceLastChange > 1
206
+ && this.context.time.smoothedFps >= 59
207
+ && composer.multisampling < this.context.renderer.capabilities.maxSamples
208
+ && this.context.time.realtimeSinceStartup - this._multisampleAutoDecreaseTime > 10
209
+ ) {
210
+ this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup;
211
+ composer.multisampling = composer.multisampling <= 0 ? 1 : composer.multisampling * 2;
212
+ composer.multisampling = Math.floor(composer.multisampling);
213
+ if (debug) console.debug(`[PostProcessing] Increased multisampling from ${prev} to ${composer.multisampling}`);
214
+ }
215
+ }
216
+ }
217
+ else {
218
+ composer.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
219
+ }
220
+
167
221
  // only set the main camera if any pass has a different camera
168
222
  // trying to avoid doing this regularly since it involves doing potentially unnecessary work
169
223
  // https://github.com/pmndrs/postprocessing/blob/3d3df0576b6d49aec9e763262d5a1ff7429fd91a/src/core/EffectComposer.js#L406
@@ -222,8 +276,30 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
222
276
  if (this._activeEffects.length > 0) {
223
277
  if (!this._postprocessing)
224
278
  this._postprocessing = new PostProcessingHandler(this.context);
225
- this._postprocessing.apply(this._activeEffects);
226
- this._applyPostQueue();
279
+
280
+ this._postprocessing.apply(this._activeEffects)
281
+ ?.then(() => {
282
+ if (!this.activeAndEnabled) return;
283
+
284
+ this._applyPostQueue();
285
+
286
+ const composer = this._postprocessing?.composer;
287
+ if (composer) {
288
+ if (this.multisampling === "auto") {
289
+ composer.multisampling = DeviceUtilities.isMobileDevice()
290
+ ? 2
291
+ : 4;
292
+ }
293
+ else {
294
+ composer.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
295
+ }
296
+ if (debug) console.debug(`[PostProcessing] Set multisampling to ${composer.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`);
297
+ }
298
+ else if (debug) {
299
+ console.warn(`[PostProcessing] No composer found`);
300
+ }
301
+ })
302
+
227
303
  }
228
304
 
229
305
  }
@@ -141,14 +141,14 @@ export class Canvas extends UIRootComponent implements ICanvas {
141
141
  }
142
142
 
143
143
  start() {
144
- this.onUpdateRenderMode();
144
+ this.applyRenderSettings();
145
145
  }
146
146
 
147
147
  onEnable() {
148
148
  super.onEnable();
149
149
  this._updateRenderSettingsRoutine = undefined;
150
150
  this._lastMatrixWorld = new Matrix4();
151
- this.onUpdateRenderMode();
151
+ this.applyRenderSettings();
152
152
  document.addEventListener("resize", this._boundRenderSettingsChanged);
153
153
  // We want to run AFTER all regular onBeforeRender callbacks
154
154
  this.context.pre_render_callbacks.push(this.onBeforeRenderRoutine);