@@ -42,7 +42,19 @@
|
|
42
42
|
return this.clock.elapsedTime;
|
43
43
|
}
|
44
44
|
|
45
|
-
/**
|
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() {
|
@@ -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
|
-
|
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
|
-
|
227
|
+
composer.addPass(ef);
|
234
228
|
}
|
235
229
|
}
|
236
230
|
|
@@ -573,7 +573,7 @@
|
|
573
573
|
this.markNeedsUpdate();
|
574
574
|
}
|
575
575
|
|
576
|
-
updateGeometry(geo: BufferGeometry,
|
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
|
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
|
-
|
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
|
|
@@ -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
|
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
|
-
|
226
|
-
this.
|
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
|
}
|