Needle Engine

Changes between version 4.3.1 and 4.3.2-beta
Files changed (4) hide show
  1. src/engine/engine_time.ts +14 -2
  2. src/engine-components/postprocessing/PostProcessingHandler.ts +8 -14
  3. src/engine-components/RendererInstancing.ts +6 -57
  4. src/engine-components/postprocessing/Volume.ts +82 -6
src/engine/engine_time.ts CHANGED
@@ -42,7 +42,19 @@
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 @@
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() {
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -43,20 +43,20 @@
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 @@
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 @@
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 @@
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
 
src/engine-components/RendererInstancing.ts CHANGED
@@ -573,7 +573,7 @@
573
573
  this.markNeedsUpdate();
574
574
  }
575
575
 
576
- updateGeometry(geo: BufferGeometry, index: number) {
576
+ updateGeometry(geo: BufferGeometry, geometryIndex: number): boolean {
577
577
  if (!this.validateGeometry(geo)) {
578
578
  return false;
579
579
  }
@@ -582,10 +582,11 @@
582
582
  this.grow(geo);
583
583
  }
584
584
  if (debugInstancing)
585
- console.debug("UPDATE MESH", index, this._batchedMesh["_geometryCount"], geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0);
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);
586
586
 
587
-
588
- 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);
589
590
  this.markNeedsUpdate();
590
591
  return true;
591
592
  }
@@ -692,8 +693,6 @@
692
693
 
693
694
  // since we have a new batched mesh we need to re-add all the instances
694
695
  // fixes https://linear.app/needle/issue/NE-5711
695
- this._usedBuckets.length = 0;
696
- this._availableBuckets.length = 0;
697
696
 
698
697
  // add current instances to new instanced mesh
699
698
  const original = [...this._handles];
@@ -758,9 +757,6 @@
758
757
  }
759
758
 
760
759
 
761
- private readonly _availableBuckets = new Array<BucketInfo>();
762
- private readonly _usedBuckets = new Array<BucketInfo>();
763
-
764
760
  private addGeometry(handle: InstanceHandle) {
765
761
 
766
762
  const obj = handle.object;
@@ -770,51 +766,11 @@
770
766
  return;
771
767
  }
772
768
 
773
- // if (handle.reservedVertexCount <= 0 || handle.reservedIndexCount <= 0) {
774
- // console.error("Cannot add geometry with 0 vertices or indices", handle.name);
775
- // return;
776
- // }
777
- // search the smallest available bucket that fits our handle
778
- let smallestBucket: BucketInfo | null = null;
779
- let smallestBucketIndex = -1;
780
- for (let i = this._availableBuckets.length - 1; i >= 0; i--) {
781
- const bucket = this._availableBuckets[i];
782
- if (bucket.vertexCount >= handle.maxVertexCount && bucket.indexCount >= handle.maxIndexCount) {
783
- if (smallestBucket == null || bucket.vertexCount < smallestBucket.vertexCount) {
784
- smallestBucket = bucket;
785
- smallestBucketIndex = i;
786
- }
787
- }
788
- }
789
- // if we have a bucket that is big enough, use it
790
- if (smallestBucket != null) {
791
- const bucket = smallestBucket;
792
- if (debugInstancing)
793
- console.debug(`[Instancing] SET GEOMETRY \"${handle.name}\"\nGEOMETRY_ID=${bucket.geometryIndex}\n${this._currentInstanceCount} instances\n${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, `);
794
- try {
795
- this._batchedMesh.setGeometryAt(bucket.geometryIndex, handle.object.geometry as BufferGeometry);
796
- const newIndex = this._batchedMesh.addInstance(bucket.geometryIndex);
797
- this._batchedMesh.setMatrixAt(newIndex, handle.object.matrixWorld);
798
- this._batchedMesh.setVisibleAt(newIndex, true);
799
- handle.__instanceIndex = newIndex;
800
- this._usedBuckets[bucket.geometryIndex] = bucket;
801
- this._availableBuckets.splice(smallestBucketIndex, 1);
802
- return;
803
- }
804
- catch (err) {
805
- if (debugInstancing)
806
- console.error("Failed to re-use space", err);
807
- else if (isDevEnvironment()) {
808
- console.warn(`Failed to re-use space \"${err instanceof Error ? err.message : err}\" in bucket ${bucket.geometryIndex} (${bucket.vertexCount}) - will add new geometry instead`);
809
- }
810
- }
811
- }
812
-
813
769
  // otherwise add more geometry / instances
814
770
  let geometryId = this._geometryIds.get(geo);
815
771
  if (geometryId === undefined || geometryId === null) {
816
772
  if (debugInstancing)
817
- console.debug(`[Instancing] > ADD GEOMETRY \"${handle.name}\"\n${this._currentInstanceCount} instances, ${handle.maxVertexCount} max vertices, ${handle.maxIndexCount} max indices`);
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`);
818
774
 
819
775
  geometryId = this._batchedMesh.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount);
820
776
  this._geometryIds.set(geo, geometryId);
@@ -829,7 +785,6 @@
829
785
  handle.__instanceIndex = i;
830
786
  handle.__reservedVertexRange = handle.maxVertexCount;
831
787
  handle.__reservedIndexRange = handle.maxIndexCount;
832
- this._usedBuckets[i] = { geometryIndex: geometryId, vertexCount: handle.maxVertexCount, indexCount: handle.maxIndexCount };
833
788
  this._batchedMesh.setMatrixAt(i, handle.object.matrixWorld);
834
789
  if (debugInstancing)
835
790
  console.debug(`[Instancing] > ADDED INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
@@ -842,7 +797,6 @@
842
797
  console.warn("Cannot remove geometry, instance index is invalid", handle.name);
843
798
  return;
844
799
  }
845
- this._usedBuckets.splice(handle.__instanceIndex, 1);
846
800
  // deleteGeometry is currently not useable since there's no optimize method
847
801
  // https://github.com/mrdoob/three.js/issues/27985
848
802
  // if (del)
@@ -853,11 +807,6 @@
853
807
  console.debug(`[Instancing] < REMOVE INSTANCE \"${handle.name}\" at [${handle.__instanceIndex}]\nGEOMETRY_ID=${handle.__geometryIndex}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`);
854
808
  }
855
809
  this._batchedMesh.deleteInstance(handle.__instanceIndex);
856
- this._availableBuckets.push({
857
- geometryIndex: handle.__geometryIndex,
858
- vertexCount: handle.reservedVertexCount,
859
- indexCount: handle.reservedIndexCount
860
- });
861
810
  }
862
811
  }
863
812
 
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -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";
@@ -59,6 +60,16 @@
59
60
  sharedProfile?: VolumeProfile;
60
61
 
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
+
72
+ /**
62
73
  * Add a post processing effect to the stack and schedules the effect stack to be re-created.
63
74
  */
64
75
  addEffect<T extends PostProcessingEffect | Effect>(effect: T): T {
@@ -66,12 +77,15 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
  }