@needle-tools/engine 4.6.1 → 4.6.2

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 (103) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/generateMeshBVH.worker-BaNp_Xtp.js +25 -0
  3. package/dist/{gltf-progressive-Bm9eEfgu.min.js → gltf-progressive-Bl4okF1b.min.js} +1 -1
  4. package/dist/{gltf-progressive-GjIqwSG3.js → gltf-progressive-DSpdn0QT.js} +2 -2
  5. package/dist/{gltf-progressive-Dn6o99rH.umd.cjs → gltf-progressive-P8b8a0qY.umd.cjs} +1 -1
  6. package/dist/needle-engine.bundle-D0XWaCQs.min.js +1575 -0
  7. package/dist/{needle-engine.bundle-BVg46UWZ.js → needle-engine.bundle-DGcStTA7.js} +9715 -9555
  8. package/dist/needle-engine.bundle-DmYMLdWP.umd.cjs +1575 -0
  9. package/dist/needle-engine.js +352 -351
  10. package/dist/needle-engine.min.js +1 -1
  11. package/dist/needle-engine.umd.cjs +1 -1
  12. package/dist/{postprocessing-CRQa6Qxn.umd.cjs → postprocessing-CjW23fio.umd.cjs} +18 -18
  13. package/dist/{postprocessing-D6W1EyZ-.js → postprocessing-DYLNOL3W.js} +4 -3
  14. package/dist/{postprocessing-DF8AlRgW.min.js → postprocessing-xYQWCHFu.min.js} +26 -26
  15. package/dist/{three-DMrv-4ar.umd.cjs → three-B_hneGZr.umd.cjs} +4 -4
  16. package/dist/{three-Bz6X1mrw.js → three-DrqIzZTH.js} +4198 -4198
  17. package/dist/{three-Boa-jOq-.min.js → three-DuDKwKB8.min.js} +33 -33
  18. package/dist/{three-examples-GggCDHv0.js → three-examples-B50TT3Iu.js} +5 -5
  19. package/dist/{three-examples-DuVhxqft.min.js → three-examples-DaDLBuy6.min.js} +14 -14
  20. package/dist/{three-examples-C7ryg8vN.umd.cjs → three-examples-X3OadjXB.umd.cjs} +3 -3
  21. package/dist/{three-mesh-ui-CY6Izc7C.min.js → three-mesh-ui-B3p3gyUz.min.js} +1 -1
  22. package/dist/{three-mesh-ui-CwlN0FUC.umd.cjs → three-mesh-ui-CQiIQIlA.umd.cjs} +1 -1
  23. package/dist/{three-mesh-ui-CLNOfsWn.js → three-mesh-ui-CxuWt7m-.js} +1 -1
  24. package/dist/{vendor-zxXa3Dmr.min.js → vendor-BlSxe9JJ.min.js} +3 -3
  25. package/dist/{vendor-BSD1RQIh.js → vendor-BmYIgaS1.js} +3 -3
  26. package/dist/{vendor-DHr4aqIZ.umd.cjs → vendor-Cavtu3CP.umd.cjs} +3 -3
  27. package/lib/engine/engine_context.d.ts +4 -1
  28. package/lib/engine/engine_context.js +9 -2
  29. package/lib/engine/engine_context.js.map +1 -1
  30. package/lib/engine/engine_three_utils.d.ts +17 -14
  31. package/lib/engine/engine_three_utils.js +106 -53
  32. package/lib/engine/engine_three_utils.js.map +1 -1
  33. package/lib/engine/engine_tonemapping.d.ts +4 -0
  34. package/lib/engine/engine_tonemapping.js +21 -18
  35. package/lib/engine/engine_tonemapping.js.map +1 -1
  36. package/lib/engine/engine_utils.js.map +1 -1
  37. package/lib/engine/engine_utils_screenshot.d.ts +1 -1
  38. package/lib/engine/engine_utils_screenshot.js +11 -2
  39. package/lib/engine/engine_utils_screenshot.js.map +1 -1
  40. package/lib/engine/webcomponents/needle-engine.d.ts +4 -1
  41. package/lib/engine/webcomponents/needle-engine.extras.js +3 -3
  42. package/lib/engine/webcomponents/needle-engine.extras.js.map +1 -1
  43. package/lib/engine/webcomponents/needle-engine.js +11 -21
  44. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  45. package/lib/engine-components/CameraUtils.js.map +1 -1
  46. package/lib/engine-components/postprocessing/Effects/Antialiasing.js +3 -1
  47. package/lib/engine-components/postprocessing/Effects/Antialiasing.js.map +1 -1
  48. package/lib/engine-components/postprocessing/Effects/BloomEffect.d.ts +2 -2
  49. package/lib/engine-components/postprocessing/Effects/BloomEffect.js +5 -2
  50. package/lib/engine-components/postprocessing/Effects/BloomEffect.js.map +1 -1
  51. package/lib/engine-components/postprocessing/Effects/ColorAdjustments.d.ts +8 -0
  52. package/lib/engine-components/postprocessing/Effects/ColorAdjustments.js +27 -8
  53. package/lib/engine-components/postprocessing/Effects/ColorAdjustments.js.map +1 -1
  54. package/lib/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.js +1 -0
  55. package/lib/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.js.map +1 -1
  56. package/lib/engine-components/postprocessing/Effects/Sharpening.d.ts +1 -0
  57. package/lib/engine-components/postprocessing/Effects/Sharpening.js +4 -0
  58. package/lib/engine-components/postprocessing/Effects/Sharpening.js.map +1 -1
  59. package/lib/engine-components/postprocessing/Effects/Tonemapping.d.ts +2 -9
  60. package/lib/engine-components/postprocessing/Effects/Tonemapping.js +23 -71
  61. package/lib/engine-components/postprocessing/Effects/Tonemapping.js.map +1 -1
  62. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +13 -0
  63. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.js +52 -0
  64. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.js.map +1 -0
  65. package/lib/engine-components/postprocessing/PostProcessingEffect.d.ts +18 -0
  66. package/lib/engine-components/postprocessing/PostProcessingEffect.js +22 -3
  67. package/lib/engine-components/postprocessing/PostProcessingEffect.js.map +1 -1
  68. package/lib/engine-components/postprocessing/PostProcessingHandler.d.ts +20 -4
  69. package/lib/engine-components/postprocessing/PostProcessingHandler.js +209 -112
  70. package/lib/engine-components/postprocessing/PostProcessingHandler.js.map +1 -1
  71. package/lib/engine-components/postprocessing/Volume.d.ts +2 -1
  72. package/lib/engine-components/postprocessing/Volume.js +51 -33
  73. package/lib/engine-components/postprocessing/Volume.js.map +1 -1
  74. package/lib/engine-components/postprocessing/index.d.ts +1 -0
  75. package/lib/engine-components/postprocessing/index.js +1 -0
  76. package/lib/engine-components/postprocessing/index.js.map +1 -1
  77. package/lib/engine-components/postprocessing/utils.d.ts +43 -0
  78. package/lib/engine-components/postprocessing/utils.js +82 -0
  79. package/lib/engine-components/postprocessing/utils.js.map +1 -1
  80. package/package.json +1 -1
  81. package/src/engine/engine_context.ts +13 -4
  82. package/src/engine/engine_three_utils.ts +134 -58
  83. package/src/engine/engine_tonemapping.ts +23 -24
  84. package/src/engine/engine_utils.ts +2 -2
  85. package/src/engine/engine_utils_screenshot.ts +13 -3
  86. package/src/engine/webcomponents/needle-engine.extras.ts +3 -3
  87. package/src/engine/webcomponents/needle-engine.ts +14 -25
  88. package/src/engine-components/CameraUtils.ts +3 -3
  89. package/src/engine-components/postprocessing/Effects/Antialiasing.ts +3 -1
  90. package/src/engine-components/postprocessing/Effects/BloomEffect.ts +6 -4
  91. package/src/engine-components/postprocessing/Effects/ColorAdjustments.ts +24 -13
  92. package/src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts +1 -0
  93. package/src/engine-components/postprocessing/Effects/Sharpening.ts +5 -0
  94. package/src/engine-components/postprocessing/Effects/Tonemapping.ts +26 -80
  95. package/src/engine-components/postprocessing/Effects/Tonemapping.utils.ts +60 -0
  96. package/src/engine-components/postprocessing/PostProcessingEffect.ts +23 -3
  97. package/src/engine-components/postprocessing/PostProcessingHandler.ts +239 -119
  98. package/src/engine-components/postprocessing/Volume.ts +54 -38
  99. package/src/engine-components/postprocessing/index.ts +2 -1
  100. package/src/engine-components/postprocessing/utils.ts +102 -2
  101. package/dist/generateMeshBVH.worker-Cdfpaq5W.js +0 -25
  102. package/dist/needle-engine.bundle-AOXFIsYk.umd.cjs +0 -1563
  103. package/dist/needle-engine.bundle-Dt52m2jf.min.js +0 -1563
@@ -1,24 +1,26 @@
1
1
  import type { Effect, EffectComposer, Pass, ToneMappingEffect as _TonemappingEffect } from "postprocessing";
2
- import { HalfFloatType, NoToneMapping, Camera as Camera3 } from "three";
2
+ import { Camera as Camera3, DepthTexture, HalfFloatType, LinearFilter, NoToneMapping, Scene, Texture, ToneMapping, WebGLRenderTarget } from "three";
3
3
 
4
4
  import { showBalloonWarning } from "../../engine/debug/index.js";
5
5
  // import { internal_SetSharpeningEffectModule } from "./Effects/Sharpening.js";
6
6
  import { MODULES } from "../../engine/engine_modules.js";
7
7
  import { Context } from "../../engine/engine_setup.js";
8
- import type { Constructor } from "../../engine/engine_types.js";
9
- import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
8
+ import { Graphics } from "../../engine/engine_three_utils.js";
9
+ import { getParam } from "../../engine/engine_utils.js";
10
10
  import { Camera } from "../Camera.js";
11
+ import { threeToneMappingToEffectMode } from "./Effects/Tonemapping.utils.js";
11
12
  import { PostProcessingEffect, PostProcessingEffectContext } from "./PostProcessingEffect.js";
13
+ import { orderEffects, PostprocessingEffectData, PostProcessingEffectOrder } from "./utils.js";
12
14
 
13
15
  declare const NEEDLE_USE_POSTPROCESSING: boolean;
14
16
  globalThis["NEEDLE_USE_POSTPROCESSING"] = globalThis["NEEDLE_USE_POSTPROCESSING"] !== undefined ? globalThis["NEEDLE_USE_POSTPROCESSING"] : true;
15
17
 
16
18
 
17
19
  const debug = getParam("debugpost");
18
- const dontMergePasses = getParam("debugpostpasses");
19
20
 
20
21
  const activeKey = Symbol("needle:postprocessing-handler");
21
22
  const autoclearSetting = Symbol("needle:previous-autoclear-state");
23
+ const previousToneMapping = Symbol("needle:previous-tone-mapping");
22
24
 
23
25
  /**
24
26
  * PostProcessingHandler is responsible for applying post processing effects to the scene. It is internally used by the {@link Volume} component
@@ -27,7 +29,15 @@ export class PostProcessingHandler {
27
29
 
28
30
  private _composer: EffectComposer | null = null;
29
31
  private _lastVolumeComponents?: PostProcessingEffect[];
30
- private _effects: Array<Effect | Pass> = [];
32
+ private readonly _effects: Array<PostprocessingEffectData> = [];
33
+
34
+ /**
35
+ * Returns true if a specific effect is currently active in the post processing stack.
36
+ */
37
+ getEffectIsActive(effect: Effect): boolean {
38
+ if (!effect) return false;
39
+ return this._isActive && this._effects.some(e => e.effect === effect);
40
+ }
31
41
 
32
42
  get isActive() {
33
43
  return this._isActive;
@@ -60,7 +70,7 @@ export class PostProcessingHandler {
60
70
  return this.onApply(this.context, components);
61
71
  }
62
72
 
63
- unapply() {
73
+ unapply(dispose: boolean = true) {
64
74
  if (debug) console.log("Unapplying postprocessing effects");
65
75
  this._isActive = false;
66
76
  if (this._lastVolumeComponents) {
@@ -73,21 +83,29 @@ export class PostProcessingHandler {
73
83
  const active = context[activeKey] as PostProcessingHandler | null;
74
84
  if (active === this) {
75
85
  delete context[activeKey];
86
+
87
+ // Restore the auto clear setting
88
+ if (typeof context.renderer[autoclearSetting] === "boolean") {
89
+ context.renderer.autoClear = context.renderer[autoclearSetting];
90
+ }
91
+ if (typeof context.renderer[previousToneMapping] === "number") {
92
+ context.renderer.toneMapping = context.renderer[previousToneMapping] as ToneMapping;
93
+ }
76
94
  }
95
+
96
+ this._composer?.removeAllPasses();
97
+ if (dispose) this._composer?.dispose();
98
+
77
99
  if (context.composer === this._composer) {
78
- context.composer?.dispose();
79
100
  context.composer = null;
80
101
  }
81
- if (typeof context.renderer[autoclearSetting] === "boolean") {
82
- context.renderer.autoClear = context.renderer[autoclearSetting];
83
- }
84
102
  }
85
103
 
86
104
  dispose() {
87
- this.unapply();
105
+ this.unapply(true);
88
106
 
89
107
  for (const effect of this._effects) {
90
- effect.dispose();
108
+ effect.effect.dispose();
91
109
  }
92
110
  this._effects.length = 0;
93
111
  this._composer = null;
@@ -105,6 +123,7 @@ export class PostProcessingHandler {
105
123
  // import("./Effects/Sharpening.effect")
106
124
  ]);
107
125
 
126
+
108
127
  // try {
109
128
  // internal_SetSharpeningEffectModule(modules[2]);
110
129
  // }
@@ -141,10 +160,22 @@ export class PostProcessingHandler {
141
160
  // apply or collect effects
142
161
  const res = component.apply(ctx);
143
162
  if (!res) continue;
163
+
164
+
144
165
  if (Array.isArray(res)) {
145
- this._effects.push(...res);
166
+ for (const effect of res) {
167
+ this._effects.push({
168
+ effect,
169
+ priority: component.order
170
+ });
171
+ }
172
+ }
173
+ else {
174
+ this._effects.push({
175
+ effect: res,
176
+ priority: component.order
177
+ });
146
178
  }
147
- else this._effects.push(res);
148
179
  }
149
180
  }
150
181
  else {
@@ -153,23 +184,41 @@ export class PostProcessingHandler {
153
184
  }
154
185
  }
155
186
 
156
- // Ensure that we have a tonemapping effect if the renderer is set to use a tone mapping
157
- if (this.context.renderer.toneMapping != NoToneMapping) {
158
- if (!this._effects.find(e => e instanceof MODULES.POSTPROCESSING.MODULE.ToneMappingEffect)) {
159
- const tonemapping = new MODULES.POSTPROCESSING.MODULE.ToneMappingEffect();
160
- this._effects.push(tonemapping);
161
- }
162
- }
163
-
164
187
  this.applyEffects(context);
165
188
  }
166
189
 
190
+ private _anyPassHasDepth = false;
191
+ private _anyPassHasNormal = false;
192
+ private _hasSmaaEffect = false;
193
+
194
+ get anyPassHasDepth() { return this._anyPassHasDepth; }
195
+ get anyPassHasNormal() { return this._anyPassHasNormal; }
196
+ get hasSmaaEffect() { return this._hasSmaaEffect; }
197
+
198
+
199
+
200
+ private _customInputBuffer: WebGLRenderTarget<Texture> | null = null;
201
+ private _customInputBufferId = 0;
202
+ private _multisampling: number = 0;
203
+ set multisampling(value: number) {
204
+ this._multisampling = value;
205
+ }
206
+ get multisampling() {
207
+ return this._multisampling;
208
+ }
209
+
167
210
 
168
211
  /** Build composer passes */
169
212
  private applyEffects(context: Context) {
213
+ // Reset state
214
+ this._anyPassHasDepth = false;
215
+ this._anyPassHasNormal = false;
216
+ this._hasSmaaEffect = false;
217
+
218
+ if (this._effects.length <= 0) {
219
+ return;
220
+ }
170
221
 
171
- const effectsOrPasses = this._effects;
172
- if (effectsOrPasses.length <= 0) return;
173
222
  const camera = context.mainCameraComponent as Camera;
174
223
  const renderer = context.renderer;
175
224
  const scene = context.scene;
@@ -178,8 +227,27 @@ export class PostProcessingHandler {
178
227
  // Store the auto clear setting because the postprocessing composer just disables it
179
228
  // and when we disable postprocessing we want to restore the original setting
180
229
  // https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12
230
+ // First we need to get the previously set autoClear setting, if it exists
231
+ if (typeof renderer[autoclearSetting] === "boolean") {
232
+ renderer.autoClear = renderer[autoclearSetting];
233
+ }
181
234
  renderer[autoclearSetting] = renderer.autoClear;
182
235
 
236
+ if (typeof renderer[previousToneMapping] === "number") {
237
+ renderer.toneMapping = renderer[previousToneMapping] as ToneMapping;
238
+ }
239
+ renderer[previousToneMapping] = renderer.toneMapping;
240
+
241
+ // Ensure that we have a tonemapping effect if the renderer is set to use a tone mapping
242
+ if (renderer.toneMapping != NoToneMapping) {
243
+ if (!this._effects.find(e => e instanceof MODULES.POSTPROCESSING.MODULE.ToneMappingEffect)) {
244
+ const tonemapping = new MODULES.POSTPROCESSING.MODULE.ToneMappingEffect();
245
+ tonemapping.name = `ToneMapping (${renderer.toneMapping})`;
246
+ tonemapping.mode = threeToneMappingToEffectMode(renderer.toneMapping);
247
+ this._effects.push({ effect: tonemapping, priority: PostProcessingEffectOrder.ToneMapping });
248
+ }
249
+ }
250
+
183
251
  // create composer and set active on context
184
252
  if (!this._composer) {
185
253
  // const hdrRenderTarget = new WebGLRenderTarget(window.innerWidth, window.innerHeight, { type: HalfFloatType });
@@ -192,12 +260,14 @@ export class PostProcessingHandler {
192
260
  if (context.composer && context.composer !== this._composer) {
193
261
  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
262
  }
263
+
195
264
  context.composer = this._composer;
196
265
  const composer = context.composer;
197
266
  composer.setMainCamera(cam);
198
267
  composer.setRenderer(renderer);
199
268
  composer.setMainScene(scene);
200
269
  composer.autoRenderToScreen = true; // Must be enabled because it might be disabled during addPass by the composer itself (depending on the effect's settings or order)
270
+ composer.multisampling = 0; // Disable multisampling by default
201
271
 
202
272
  for (const prev of composer.passes)
203
273
  prev.dispose();
@@ -205,81 +275,168 @@ export class PostProcessingHandler {
205
275
 
206
276
  // Render to screen pass
207
277
  const screenpass = new MODULES.POSTPROCESSING.MODULE.RenderPass(scene, cam);
208
- screenpass.name = "Render To Screen";
278
+ screenpass.name = "RenderPass";
209
279
  screenpass.mainScene = scene;
210
280
  composer.addPass(screenpass);
211
281
 
212
- const automaticEffectsOrdering = true;
213
- if (automaticEffectsOrdering && !dontMergePasses) {
214
- try {
215
- this.orderEffects();
282
+ const screenPassRender = screenpass.render;
283
+ this._customInputBuffer?.dispose();
284
+ this._customInputBuffer = null;
285
+ screenpass.render = (renderer, inputBuffer, outputBuffer, deltaTime, stencilTest) => {
286
+ if (!inputBuffer) return;
216
287
 
217
- const effects: Array<Effect> = [];
288
+ // screenPassRender.call(screenpass, renderer, inputBuffer, outputBuffer, deltaTime, stencilTest);
289
+ // return;
218
290
 
219
- for (const ef of effectsOrPasses) {
220
- if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect) {
221
- effects.push(ef as Effect);
222
- }
223
- else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
224
- ef.renderToScreen = false;
225
- composer.addPass(ef as Pass);
226
- }
227
- else {
228
- // seems some effects are not correctly typed, but three can deal with them,
229
- // so we might need to just pass them through
230
- composer.addPass(ef);
231
- }
232
- }
291
+ // Make sure multisampling is disabled on the composer buffers. Technically a user could still set multisampling directly on the composer so this is to override that and make sure these textures do NOT use multisampling
292
+ inputBuffer.samples = 0;
293
+ if (outputBuffer) {
294
+ outputBuffer.samples = 0;
295
+ }
233
296
 
234
- // create and apply uber pass
235
- if (effects.length > 0) {
236
- const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
237
- pass.name = effects.map(e => e.name).join(", ");
238
- pass.mainScene = scene;
239
- pass.enabled = true;
240
- pass.renderToScreen = false;
241
- composer.addPass(pass);
297
+ // Make sure the input buffer is a WebGLRenderTarget with the correct settings
298
+ if (!this._customInputBuffer
299
+ || this._customInputBuffer.width !== inputBuffer.width
300
+ || this._customInputBuffer.height !== inputBuffer.height
301
+ || this._customInputBuffer.samples !== this._multisampling
302
+ || this._customInputBuffer.texture.format !== inputBuffer.texture.format
303
+ || this._customInputBuffer.texture.type !== HalfFloatType
304
+ ) {
305
+ this._customInputBuffer?.dispose();
306
+
307
+ this._customInputBuffer = new WebGLRenderTarget(inputBuffer.width, inputBuffer.height, {
308
+ format: inputBuffer.texture.format,
309
+ type: HalfFloatType,
310
+ depthBuffer: inputBuffer.depthBuffer,
311
+ depthTexture: inputBuffer.depthTexture
312
+ ? new DepthTexture(inputBuffer.width, inputBuffer.height)
313
+ : undefined,
314
+ stencilBuffer: inputBuffer.stencilBuffer,
315
+ samples: Math.max(0, this._multisampling),
316
+ minFilter: inputBuffer.texture.minFilter ?? LinearFilter,
317
+ magFilter: inputBuffer.texture.magFilter ?? LinearFilter,
318
+ generateMipmaps: inputBuffer.texture.generateMipmaps,
319
+ });
320
+ this._customInputBufferId++;
321
+ this._customInputBuffer.texture.name = `CustomInputBuffer-${this._customInputBufferId}`;
322
+ if (this._customInputBuffer.depthTexture && inputBuffer.depthTexture) {
323
+ this._customInputBuffer.depthTexture.format = inputBuffer.depthTexture.format;
324
+ this._customInputBuffer.depthTexture.type = inputBuffer.depthTexture.type;
242
325
  }
326
+ // https://github.com/pmndrs/postprocessing/blob/ad338df710ef41fee4e5d10ad2c2c299030d46ef/src/core/EffectComposer.js#L366
327
+ if (this._customInputBuffer.samples > 0)
328
+ (this._customInputBuffer as any).ignoreDepthForMultisampleCopy = false;
329
+
330
+ if (debug) console.warn(`[PostProcessing] Input buffer created with size ${this._customInputBuffer.width}x${this._customInputBuffer.height} and samples ${this._customInputBuffer.samples}`);
243
331
  }
244
- catch (e) {
245
- console.error("Error while applying postprocessing effects", e);
246
- composer.removeAllPasses();
332
+ // Calling the original render function with the input buffer
333
+ screenPassRender.call(screenpass, renderer, this._customInputBuffer, outputBuffer, deltaTime, stencilTest);
334
+ // Blit the resulting buffer to the input buffer passed in by the composer so it's used for subsequent effects
335
+ Graphics.blit(this._customInputBuffer.texture, inputBuffer, {
336
+ renderer,
337
+ depthTexture: this._customInputBuffer.depthTexture,
338
+ depthWrite: true,
339
+ depthTest: true,
340
+ });
341
+ };
342
+
343
+
344
+ try {
345
+ orderEffects(this._effects);
346
+
347
+ let foundTonemappingEffect = false;
348
+ let activeTonemappingEffect: _TonemappingEffect | null = null;
349
+ for (let i = this._effects.length - 1; i >= 0; i--) {
350
+ const ef = this._effects[i].effect;
351
+ if (ef instanceof MODULES.POSTPROCESSING.MODULE.ToneMappingEffect) {
352
+ // If we already have a tonemapping effect, we can skip this one
353
+ if (foundTonemappingEffect) {
354
+ if (debug) console.warn(`[PostProcessing] Found multiple tonemapping effects in the scene: ${ef.name} and ${activeTonemappingEffect?.name}. Only the last one added will be used.`);
355
+ this._effects.splice(i, 1);
356
+ continue;
357
+ }
358
+ activeTonemappingEffect = ef;
359
+ foundTonemappingEffect = true;
360
+ }
247
361
  }
248
- }
249
- else {
250
- // we still want to sort passes, but we do not want to merge them for debugging
251
- if (automaticEffectsOrdering)
252
- this.orderEffects();
253
362
 
254
- for (const ef of effectsOrPasses) {
363
+ const effectsToMerge: Array<Effect> = [];
364
+ let hasConvolutionEffectInArray = false;
365
+
366
+ for (const entry of this._effects) {
367
+ const ef = entry.effect;
368
+
369
+ if (ef instanceof MODULES.POSTPROCESSING.MODULE.SMAAEffect) {
370
+ this._hasSmaaEffect = true;
371
+ }
372
+ else if (ef instanceof MODULES.POSTPROCESSING.MODULE.NormalPass) {
373
+ this._anyPassHasNormal = true;
374
+ }
375
+
376
+
377
+ // There can be only one tonemapping effect in the scene, so we can skip all others
378
+ if (ef instanceof MODULES.POSTPROCESSING.MODULE.ToneMappingEffect && activeTonemappingEffect !== ef) {
379
+ // If we already have a tonemapping effect, we can skip this one
380
+ continue;
381
+ }
382
+
383
+ // We can also not merge multiple effects of the same type in one pass
384
+ // So we first need to create a new pass with whatever effects we have so far
385
+ // TODO: this seems to work fine for some effects (like ColorAdjustments) and only caused issues with multiple Tonemapping effects so far which is handled above
386
+ // const constructor = ef.constructor as Constructor<Effect | Pass>;
387
+ // if(effectsToMerge.find(e => e.constructor === constructor)) {
388
+ // // this.createPassForMergeableEffects(effectsToMerge, composer, cam, scene);
389
+ // }
390
+
255
391
  if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect) {
256
- const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ef as Effect);
257
- pass.name = ef.name;
258
- composer.addPass(pass);
392
+ const attributes = ef.getAttributes();
393
+ const convolution = MODULES.POSTPROCESSING.MODULE.EffectAttribute.CONVOLUTION;
394
+ if (attributes & convolution) {
395
+ if (debug) console.log("[PostProcessing] Convolution effect: " + ef.name);
396
+ if (hasConvolutionEffectInArray) {
397
+ if (debug) console.log("[PostProcessing] Merging effects with convolution", effectsToMerge.map(e => e.name).join(", "));
398
+ this.createPassForMergeableEffects(effectsToMerge, composer, cam, scene);
399
+ }
400
+ hasConvolutionEffectInArray = true;
401
+ }
402
+ // Otherwise we can merge it
403
+ effectsToMerge.push(ef as Effect);
259
404
  }
260
405
  else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
406
+ hasConvolutionEffectInArray = false;
407
+ this.createPassForMergeableEffects(effectsToMerge, composer, cam, scene);
261
408
  ef.renderToScreen = false;
262
409
  composer.addPass(ef as Pass);
263
410
  }
264
- else
411
+ else {
265
412
  // seems some effects are not correctly typed, but three can deal with them,
266
- // so we just pass them through
413
+ // so we might need to just pass them through
414
+ hasConvolutionEffectInArray = false;
415
+ this.createPassForMergeableEffects(effectsToMerge, composer, cam, scene);
267
416
  composer.addPass(ef);
417
+ }
268
418
  }
269
- }
270
419
 
420
+ this.createPassForMergeableEffects(effectsToMerge, composer, cam, scene);
421
+ }
422
+ catch (e) {
423
+ console.error("Error while applying postprocessing effects", e);
424
+ composer.removeAllPasses();
425
+ }
271
426
 
272
427
  // The last pass is the one that renders to the screen, so we need to set the gamma correction for it (and enable it for all others)
273
428
  for (let i = 0; i < composer.passes.length; i++) {
274
- const pass = composer.passes[i] as any;
429
+ const pass = composer.passes[i];
275
430
  const isLast = i === composer.passes.length - 1;
276
- if (pass?.configuration !== undefined) {
277
- pass.configuration.gammaCorrection = isLast;
431
+ if ((pass as any)?.configuration !== undefined) {
432
+ (pass as any).configuration.gammaCorrection = isLast;
278
433
  }
279
434
  else if ("autosetGamma" in pass) {
280
435
  // Some effects have a autosetGamma property that we can use to set the gamma correction
281
436
  pass.autosetGamma = isLast;
282
437
  }
438
+
439
+ this._anyPassHasDepth ||= pass.needsDepthTexture;
283
440
  }
284
441
 
285
442
  // DEBUG LAND BELOW
@@ -287,56 +444,20 @@ export class PostProcessingHandler {
287
444
  if (debug) this._onCreateEffectsDebug(this._composer!, cam);
288
445
  }
289
446
 
290
- private orderEffects() {
291
- if (debug === "verbose") console.debug("Before ordering effects", [...this._effects]);
292
-
293
- // Order of effects for correct results.
294
- // Aligned with https://github.com/pmndrs/postprocessing/wiki/Effect-Merging#effect-execution-order
295
- // We can not put this into global scope because then the module might not yet be initialized
296
- effectsOrder ??= [
297
- MODULES.POSTPROCESSING.MODULE.NormalPass,
298
- MODULES.POSTPROCESSING.MODULE.DepthDownsamplingPass,
299
- MODULES.POSTPROCESSING.MODULE.SMAAEffect,
300
- MODULES.POSTPROCESSING.MODULE.SSAOEffect,
301
- MODULES.POSTPROCESSING_AO.MODULE.N8AOPostPass,
302
- MODULES.POSTPROCESSING.MODULE.TiltShiftEffect,
303
- MODULES.POSTPROCESSING.MODULE.DepthOfFieldEffect,
304
- MODULES.POSTPROCESSING.MODULE.ChromaticAberrationEffect,
305
- MODULES.POSTPROCESSING.MODULE.BloomEffect,
306
- MODULES.POSTPROCESSING.MODULE.SelectiveBloomEffect,
307
- MODULES.POSTPROCESSING.MODULE.VignetteEffect,
308
- MODULES.POSTPROCESSING.MODULE.PixelationEffect,
309
- MODULES.POSTPROCESSING.MODULE.ToneMappingEffect,
310
- MODULES.POSTPROCESSING.MODULE.HueSaturationEffect,
311
- MODULES.POSTPROCESSING.MODULE.BrightnessContrastEffect,
312
- // __SHARPENING_MODULE._SharpeningEffect,
313
- ];
314
-
315
-
316
- // TODO: enforce correct order of effects (e.g. DOF before Bloom)
317
- const effects = this._effects;
318
- effects.sort((a, b) => {
319
- // we use find index here because sometimes constructor names are prefixed with `_`
320
- // TODO: find a more robust solution that isnt name based (not sure if that exists tho... maybe we must give effect TYPES some priority/index)
321
- const aidx = effectsOrder!.findIndex(e => a.constructor.name.endsWith(e.name));
322
- const bidx = effectsOrder!.findIndex(e => b.constructor.name.endsWith(e.name));
323
- // Unknown effects should be rendered first
324
- if (aidx < 0) {
325
- if (debug) console.warn("Unknown effect found: ", a.constructor.name);
326
- return -1;
327
- }
328
- else if (bidx < 0) {
329
- if (debug) console.warn("Unknown effect found: ", b.constructor.name);
330
- return 1;
331
- }
332
- // if (aidx < 0) return 1;
333
- // if (bidx < 0) return -1;
334
- return aidx - bidx;
335
- });
336
447
 
337
- if (debug === "verbose") console.debug("After ordering effects", [...this._effects]);
338
- }
339
448
 
449
+ /** Should be called before `composer.addPass()` to create an effect pass with all previously collected effects that can be merged up to that point */
450
+ private createPassForMergeableEffects(effects: Array<Effect>, composer: EffectComposer, camera: Camera3, scene: Scene) {
451
+ if (effects.length > 0) {
452
+ const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(camera, ...effects);
453
+ pass.name = effects.map(e => e.name).join(", ");
454
+ pass.mainScene = scene;
455
+ pass.enabled = true;
456
+ pass.renderToScreen = false;
457
+ composer.addPass(pass);
458
+ effects.length = 0; // Clear effects after adding them to the pass
459
+ }
460
+ }
340
461
 
341
462
 
342
463
 
@@ -412,4 +533,3 @@ export class PostProcessingHandler {
412
533
 
413
534
  }
414
535
 
415
- let effectsOrder: Array<Constructor<Effect | Pass>> | null = null;
@@ -1,4 +1,4 @@
1
- import type { Effect } from "postprocessing";
1
+ import type { Effect, EffectComposer } from "postprocessing";
2
2
 
3
3
  import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
4
4
  import { Context } from "../../engine/engine_context.js";
@@ -56,6 +56,9 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
56
56
  return this._activeEffects;
57
57
  }
58
58
 
59
+ get dirty() { return this._isDirty; }
60
+ set dirty(value: boolean) { this._isDirty = value; }
61
+
59
62
  @serializeable(VolumeProfile)
60
63
  sharedProfile?: VolumeProfile;
61
64
 
@@ -184,38 +187,56 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
184
187
 
185
188
  this.context.composer.setMainScene(this.context.scene);
186
189
 
187
- const composer = this.context.composer;
188
190
  if (this.multisampling === "auto") {
189
191
 
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}`);
192
+ // If the postprocessing handler is using depth+normals (e.g. with SMAA) we ALWAYS disable multisampling to avoid ugly edges
193
+ if (this._postprocessing && (this._postprocessing.hasSmaaEffect)) {
194
+ if (this._postprocessing.multisampling !== 0) {
195
+ this._postprocessing.multisampling = 0;
196
+ if (debug || isDevEnvironment()) {
197
+ console.warn(`[PostProcessing] Disabling multisampling you're using SMAA and have set the 'multisampling: auto' on your PostprocessingManager/Volume component. If you need anti-aliasing, consider adding an Antialiasing component or SMAA effect to the PostprocessingManager.`);
198
+ }
203
199
  }
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
200
+ }
201
+ else {
202
+ const timeSinceLastChange = this.context.time.realtimeSinceStartup - this._multisampleAutoChangeTime;
203
+
204
+ if (this.context.time.realtimeSinceStartup - this._componentEnabledTime > 2
205
+ && timeSinceLastChange > .5
209
206
  ) {
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}`);
207
+ const prev = this._postprocessing.multisampling;
208
+
209
+ if (this._postprocessing.multisampling > 0 && this.context.time.smoothedFps <= 50) {
210
+ this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup;
211
+ this._multisampleAutoDecreaseTime = this.context.time.realtimeSinceStartup;
212
+ let newMultiSample = this._postprocessing.multisampling * .5;
213
+ newMultiSample = Math.floor(newMultiSample);
214
+ if (newMultiSample != this._postprocessing.multisampling) {
215
+ this._postprocessing.multisampling = newMultiSample;
216
+ }
217
+ if (debug) console.debug(`[PostProcessing] Reduced multisampling from ${prev} to ${this._postprocessing.multisampling}`);
218
+ }
219
+ // if performance is good for a while try increasing multisampling again
220
+ else if (timeSinceLastChange > 1
221
+ && this.context.time.smoothedFps >= 59
222
+ && this._postprocessing.multisampling < this.context.renderer.capabilities.maxSamples
223
+ && this.context.time.realtimeSinceStartup - this._multisampleAutoDecreaseTime > 10
224
+ ) {
225
+ this._multisampleAutoChangeTime = this.context.time.realtimeSinceStartup;
226
+ let newMultiSample = this._postprocessing.multisampling <= 0 ? 1 : this._postprocessing.multisampling * 2;
227
+ newMultiSample = Math.floor(newMultiSample);
228
+ if (newMultiSample !== this._postprocessing.multisampling) {
229
+ this._postprocessing.multisampling = newMultiSample;
230
+ }
231
+ if (debug) console.debug(`[PostProcessing] Increased multisampling from ${prev} to ${this._postprocessing.multisampling}`);
232
+ }
214
233
  }
215
234
  }
216
235
  }
217
236
  else {
218
- composer.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
237
+ const newMultiSample = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
238
+ if (newMultiSample !== this._postprocessing.multisampling)
239
+ this._postprocessing.multisampling = newMultiSample;
219
240
  }
220
241
 
221
242
  // only set the main camera if any pass has a different camera
@@ -244,7 +265,7 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
244
265
  private _isDirty: boolean = false;
245
266
 
246
267
  private apply() {
247
- if (debug) console.log(`Apply PostProcessing "${this.name}"`);
268
+ if (debug) console.log(`Apply PostProcessing "${this.name || "unnamed"}"`);
248
269
 
249
270
  if (isDevEnvironment()) {
250
271
  if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) {
@@ -256,7 +277,6 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
256
277
  }
257
278
 
258
279
  this._isDirty = false;
259
- this.unapply();
260
280
 
261
281
  this._activeEffects.length = 0;
262
282
  // get from profile
@@ -283,32 +303,28 @@ export class Volume extends Behaviour implements IEditorModificationReceiver, IP
283
303
 
284
304
  this._applyPostQueue();
285
305
 
286
- const composer = this._postprocessing?.composer;
287
- if (composer) {
306
+ if (this._postprocessing) {
288
307
  if (this.multisampling === "auto") {
289
- composer.multisampling = DeviceUtilities.isMobileDevice()
308
+ this._postprocessing.multisampling = DeviceUtilities.isMobileDevice()
290
309
  ? 2
291
310
  : 4;
292
311
  }
293
312
  else {
294
- composer.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
313
+ this._postprocessing.multisampling = Math.max(0, Math.min(this.multisampling, this.context.renderer.capabilities.maxSamples));
295
314
  }
296
- if (debug) console.debug(`[PostProcessing] Set multisampling to ${composer.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`);
315
+ if (debug) console.debug(`[PostProcessing] Set multisampling to ${this._postprocessing.multisampling} (Is Mobile: ${DeviceUtilities.isMobileDevice()})`);
297
316
  }
298
317
  else if (debug) {
299
318
  console.warn(`[PostProcessing] No composer found`);
300
319
  }
301
320
  })
302
-
321
+ }
322
+ else {
323
+ this._postprocessing?.unapply(false);
303
324
  }
304
325
 
305
326
  }
306
327
 
307
- private unapply() {
308
- this._postprocessing?.unapply();
309
- }
310
-
311
-
312
328
  private _applyPostQueue() {
313
329
  if (this._modificationQueue) {
314
330
  for (const entry of this._modificationQueue.values()) this.onEditorModification(entry);