Needle Engine

Changes between version 3.43.0-beta.1 and 3.44.0-beta
Files changed (22) hide show
  1. src/engine-components/postprocessing/Effects/Antialiasing.ts +1 -1
  2. src/engine-components/postprocessing/Effects/Bloom.ts +0 -94
  3. src/engine-components/postprocessing/Effects/ChromaticAberration.ts +1 -4
  4. src/engine-components/postprocessing/Effects/ColorAdjustments.ts +13 -47
  5. src/engine-components/codegen/components.ts +2 -2
  6. src/engine-components/postprocessing/Effects/DepthOfField.ts +12 -13
  7. src/engine/engine_context.ts +1 -1
  8. src/engine/engine_tonemapping.ts +1 -1
  9. src/engine-components/postprocessing/Effects/Pixelation.ts +1 -1
  10. src/engine-components-experimental/networking/PlayerSync.ts +4 -3
  11. src/engine-components/postprocessing/PostProcessingEffect.ts +26 -54
  12. src/engine-components/postprocessing/PostProcessingHandler.ts +73 -57
  13. src/engine/codegen/register_types.ts +4 -4
  14. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts +5 -5
  15. src/engine-components/postprocessing/Effects/Sharpening.ts +26 -20
  16. src/engine-components/postprocessing/Effects/TiltShiftEffect.ts +0 -2
  17. src/engine-components/postprocessing/Effects/Tonemapping.ts +105 -33
  18. src/engine-components/postprocessing/utils.ts +7 -4
  19. src/engine-components/postprocessing/Volume.ts +3 -3
  20. src/engine-components/postprocessing/VolumeParameter.ts +55 -6
  21. src/engine-components/webxr/WebXR.ts +2 -1
  22. src/engine-components/postprocessing/Effects/BloomEffect.ts +113 -0
src/engine-components/postprocessing/Effects/Antialiasing.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  // edgeDetectionThreshold!: VolumeParameter;
25
25
 
26
26
  @serializable(VolumeParameter)
27
- preset!: VolumeParameter;
27
+ readonly preset: VolumeParameter = new VolumeParameter();
28
28
 
29
29
 
30
30
  onCreateEffect(): EffectProviderResult {
src/engine-components/postprocessing/Effects/Bloom.ts DELETED
@@ -1,94 +0,0 @@
1
- import { BlendFunction, BloomEffect, SelectiveBloomEffect } from "postprocessing";
2
- import { MathUtils } from "three";
3
-
4
- import { serializable } from "../../../engine/engine_serialization.js";
5
- import { PostProcessingEffect } from "../PostProcessingEffect.js";
6
- import { VolumeParameter } from "../VolumeParameter.js";
7
- import { registerCustomEffectType } from "../VolumeProfile.js";
8
-
9
- export class Bloom extends PostProcessingEffect {
10
-
11
- /** Whether to use selective bloom by default */
12
- static useSelectiveBloom = false;
13
-
14
- get typeName() {
15
- return "Bloom";
16
- }
17
-
18
- @serializable(VolumeParameter)
19
- threshold!: VolumeParameter;
20
- @serializable(VolumeParameter)
21
- intensity!: VolumeParameter;
22
- @serializable(VolumeParameter)
23
- scatter!: VolumeParameter;
24
-
25
- selectiveBloom?: boolean;
26
-
27
- init() {
28
- this.threshold.defaultValue = 1;
29
- this.intensity.defaultValue = 0;
30
- this.scatter.defaultValue = .2;
31
-
32
- if (this.selectiveBloom) {
33
- this.threshold.valueProcessor = (v: number) => v;
34
- this.intensity.valueProcessor = (v: number) => v;
35
- this.scatter.valueProcessor = (v: number) => 1 * Math.PI * (1 - v);
36
- }
37
- else {
38
- this.threshold.valueProcessor = (v: number) => v;
39
- this.intensity.valueProcessor = (v: number) => v;
40
- this.scatter.valueProcessor = (v: number) => 1 * Math.PI * (1 - v);
41
- }
42
- }
43
-
44
- onCreateEffect() {
45
- let bloom: BloomEffect;
46
-
47
- if (this.selectiveBloom == undefined) {
48
- this.selectiveBloom = Bloom.useSelectiveBloom;
49
- }
50
-
51
- if (this.selectiveBloom) {
52
- // https://github.com/pmndrs/postprocessing/blob/64d2829f014cfec97a46bf3c109f3abc55af0715/demo/src/demos/BloomDemo.js#L265
53
- const selectiveBloom = bloom = new SelectiveBloomEffect(this.context.scene, this.context.mainCamera!, {
54
- blendFunction: BlendFunction.ADD,
55
- mipmapBlur: true,
56
- luminanceThreshold: this.threshold.value,
57
- luminanceSmoothing: this.scatter.value,
58
- radius: 0.85, // default value
59
- intensity: this.intensity.value,
60
- });
61
- selectiveBloom.inverted = true;
62
- }
63
- else {
64
- bloom = new BloomEffect({
65
- blendFunction: BlendFunction.ADD,
66
- mipmapBlur: true,
67
- luminanceThreshold: this.threshold.value,
68
- luminanceSmoothing: this.scatter.value,
69
- radius: 0.85, // default value
70
- intensity: this.intensity.value,
71
- });
72
- }
73
-
74
- this.intensity.onValueChanged = newValue => {
75
- bloom!.intensity = newValue;
76
- };
77
- this.threshold.onValueChanged = newValue => {
78
- // for some reason the threshold needs to be gamma-corrected
79
- bloom!.luminanceMaterial.threshold = Math.pow(newValue, 2.2);
80
- };
81
- this.scatter.onValueChanged = newValue => {
82
- bloom!.luminancePass.enabled = true;
83
- console.log("Scatter", newValue);
84
- bloom!.luminanceMaterial.smoothing = newValue;
85
- if (bloom["mipmapBlurPass"])
86
- // heuristic so it looks similar to "scatter" in other engines
87
- bloom!["mipmapBlurPass"].radius = MathUtils.lerp(0.1, 0.9, 1 - newValue / Math.PI);
88
- };
89
-
90
- return bloom;
91
- }
92
-
93
- }
94
- registerCustomEffectType("Bloom", Bloom);
src/engine-components/postprocessing/Effects/ChromaticAberration.ts CHANGED
@@ -13,11 +13,8 @@
13
13
  }
14
14
 
15
15
  @serializable(VolumeParameter)
16
- intensity!: VolumeParameter;
16
+ readonly intensity: VolumeParameter = new VolumeParameter(0);
17
17
 
18
- init() {
19
- this.intensity.defaultValue = 0;
20
- }
21
18
 
22
19
  onCreateEffect(): EffectProviderResult {
23
20
  const chromatic = new ChromaticAberrationEffect();
src/engine-components/postprocessing/Effects/ColorAdjustments.ts CHANGED
@@ -1,15 +1,12 @@
1
- import { BrightnessContrastEffect, HueSaturationEffect, ToneMappingEffect, ToneMappingMode } from "postprocessing";
2
- import { ACESFilmicToneMapping, AgXToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, ReinhardToneMapping } from "three";
1
+ import { BrightnessContrastEffect, HueSaturationEffect } from "postprocessing";
2
+ import { NoToneMapping } from "three";
3
3
 
4
4
  import { serializable } from "../../../engine/engine_serialization.js";
5
- import { GameObject } from "../../Component.js";
6
5
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
7
- import { Volume } from "../Volume.js";
8
6
  import { VolumeParameter } from "../VolumeParameter.js";
9
7
  import { registerCustomEffectType } from "../VolumeProfile.js";
10
- import { ToneMapping, TonemappingMode } from "./Tonemapping.js";
8
+ import { ToneMappingEffect } from "./Tonemapping.js";
11
9
 
12
-
13
10
  export class ColorAdjustments extends PostProcessingEffect {
14
11
 
15
12
  get typeName() {
@@ -17,16 +14,16 @@
17
14
  }
18
15
 
19
16
  @serializable(VolumeParameter)
20
- postExposure!: VolumeParameter;
17
+ readonly postExposure: VolumeParameter = new VolumeParameter(0);
21
18
 
22
19
  @serializable(VolumeParameter)
23
- contrast!: VolumeParameter;
20
+ readonly contrast: VolumeParameter = new VolumeParameter(0);
24
21
 
25
22
  @serializable(VolumeParameter)
26
- hueShift!: VolumeParameter;
23
+ readonly hueShift: VolumeParameter = new VolumeParameter(0);
27
24
 
28
25
  @serializable(VolumeParameter)
29
- saturation!: VolumeParameter;
26
+ readonly saturation: VolumeParameter = new VolumeParameter(0);
30
27
 
31
28
  init() {
32
29
  this.postExposure!.valueProcessor = v => {
@@ -56,55 +53,25 @@
56
53
  this.saturation.defaultValue = 0;
57
54
  }
58
55
 
59
- unapply() {
60
- // reset to the current value in the toneMappingEffect if that exists, else Linear
61
- const currentMode: any = this.toneMappingEffect?.mode?.value;
62
- const newMode = this.toneMappingEffect?.getThreeToneMapping(currentMode)
63
- this.context.renderer.toneMapping = newMode ?? LinearToneMapping;
64
- }
65
-
66
- private threeToneMappingToEffectMode(mode: number | undefined): ToneMappingMode {
67
- switch (mode) {
68
- case LinearToneMapping: return ToneMappingMode.LINEAR;
69
- case ACESFilmicToneMapping: return ToneMappingMode.ACES_FILMIC;
70
- case AgXToneMapping: return ToneMappingMode.AGX;
71
- case NeutralToneMapping: return ToneMappingMode.NEUTRAL;
72
- case ReinhardToneMapping: return ToneMappingMode.REINHARD;
73
- default: return ToneMappingMode.LINEAR;
74
- }
75
- }
76
-
77
- private toneMappingEffect: ToneMapping | null = null;
78
56
  onCreateEffect(): EffectProviderResult {
79
57
  const effects: EffectProviderResult = [];
80
58
 
59
+ // TODO: do we still need this?
81
60
  if (this.context.renderer.toneMapping !== NoToneMapping && this.postExposure.overrideState)
82
61
  this.context.renderer.toneMapping = NoToneMapping;
83
62
 
84
63
 
85
- // workaround: find the ToneMapping effect in the scene so we can apply the mode
86
- this.toneMappingEffect = GameObject.findObjectOfType(Volume)?.sharedProfile?.components.find(c => c.typeName === "ToneMapping") as ToneMapping | null;
87
- const currentMode: any = this.toneMappingEffect?.mode?.value;
88
- const expectedThreeMode = this.toneMappingEffect?.getThreeToneMapping(currentMode);
64
+ // find the ToneMapping effect because we need it to apply post exposure
65
+ const hasTonemapping = this.postprocessingContext?.components.find(c => c instanceof ToneMappingEffect) as ToneMappingEffect;
66
+ if(!hasTonemapping){
67
+ this.postprocessingContext?.components.push(new ToneMappingEffect());
68
+ }
89
69
 
90
70
  // We need this effect if someone uses ACES or AgX tonemapping;
91
71
  // problem is that we CAN'T use this effect for the "Linear" case, the package expects that in this case you remove the effect
92
- const tonemapping = new ToneMappingEffect({
93
- mode: this.threeToneMappingToEffectMode(expectedThreeMode),
94
- // middleGrey: 0.5,
95
- // averageLuminance: 1,
96
- });
97
-
98
72
  this.postExposure!.onValueChanged = (v) => {
99
73
  if (this.postExposure.overrideState)
100
74
  this.context.renderer.toneMappingExposure = v;
101
-
102
- // this is a workaround so that we can apply tonemapping options – no access to the ToneMappingEffect instance from the Tonemapping effect right now...
103
- const currentMode = this.toneMappingEffect?.mode?.value;
104
- const threeMode = this.toneMappingEffect?.getThreeToneMapping(currentMode);
105
- const mappedMode = this.threeToneMappingToEffectMode(threeMode);
106
- if (mappedMode !== undefined)
107
- tonemapping.mode = mappedMode;
108
75
  };
109
76
 
110
77
  const brightnesscontrast = new BrightnessContrastEffect();
@@ -114,7 +81,6 @@
114
81
 
115
82
  effects.push(brightnesscontrast);
116
83
  effects.push(hueSaturationEffect);
117
- effects.push(tonemapping);
118
84
 
119
85
  this.hueShift!.onValueChanged = v => hueSaturationEffect.hue = v;
120
86
  this.saturation!.onValueChanged = v => hueSaturationEffect.saturation = v;
src/engine-components/codegen/components.ts CHANGED
@@ -31,7 +31,7 @@
31
31
  export { BasicIKConstraint } from "../BasicIKConstraint.js";
32
32
  export { BehaviorExtension } from "../export/usdz/extensions/behavior/Behaviour.js";
33
33
  export { BehaviorModel } from "../export/usdz/extensions/behavior/BehavioursBuilder.js";
34
- export { Bloom } from "../postprocessing/Effects/Bloom.js";
34
+ export { BloomEffect } from "../postprocessing/Effects/BloomEffect.js";
35
35
  export { BoxCollider } from "../Collider.js";
36
36
  export { BoxGizmo } from "../Gizmos.js";
37
37
  export { BoxHelperComponent } from "../BoxHelperComponent.js";
@@ -181,7 +181,7 @@
181
181
  export { TextExtension } from "../export/usdz/extensions/USDZText.js";
182
182
  export { TextureSheetAnimationModule } from "../ParticleSystemModules.js";
183
183
  export { TiltShiftEffect } from "../postprocessing/Effects/TiltShiftEffect.js";
184
- export { ToneMapping } from "../postprocessing/Effects/Tonemapping.js";
184
+ export { ToneMappingEffect } from "../postprocessing/Effects/Tonemapping.js";
185
185
  export { TrailModule } from "../ParticleSystemModules.js";
186
186
  export { TransformData } from "../export/usdz/extensions/Animation.js";
187
187
  export { TransformGizmo } from "../TransformGizmo.js";
src/engine-components/postprocessing/Effects/DepthOfField.ts CHANGED
@@ -25,22 +25,22 @@
25
25
  mode!: DepthOfFieldMode;
26
26
 
27
27
  @serializable(VolumeParameter)
28
- focusDistance!: VolumeParameter;
28
+ readonly focusDistance: VolumeParameter = new VolumeParameter();
29
29
 
30
30
  @serializable(VolumeParameter)
31
- focalLength!: VolumeParameter;
31
+ readonly focalLength: VolumeParameter = new VolumeParameter();
32
32
 
33
33
  @serializable(VolumeParameter)
34
- aperture!: VolumeParameter;
34
+ readonly aperture: VolumeParameter = new VolumeParameter();
35
35
 
36
36
  @serializable(VolumeParameter)
37
- gaussianMaxRadius!: VolumeParameter;
37
+ readonly gaussianMaxRadius: VolumeParameter = new VolumeParameter();
38
38
 
39
39
  @serializable(VolumeParameter)
40
- resolutionScale?: VolumeParameter;
40
+ readonly resolutionScale: VolumeParameter = new VolumeParameter(1 * 1 / window.devicePixelRatio);
41
41
 
42
42
  @serializable(VolumeParameter)
43
- bokehScale?: VolumeParameter;
43
+ readonly bokehScale: VolumeParameter = new VolumeParameter();
44
44
 
45
45
  init() {
46
46
  this.focalLength.valueProcessor = v => {
@@ -62,14 +62,13 @@
62
62
  return undefined;
63
63
  }
64
64
 
65
- const factor = 1 / window.devicePixelRatio;
65
+ // const factor = 1 / window.devicePixelRatio;
66
+ // if (this.resolutionScale === undefined) {
67
+ // let defaultValue = 1;
68
+ // if(isMobileDevice()) defaultValue = .6;
69
+ // this.resolutionScale = new VolumeParameter(defaultValue * factor);
70
+ // }
66
71
 
67
- if (this.resolutionScale === undefined) {
68
- let defaultValue = 1;
69
- if(isMobileDevice()) defaultValue = .6;
70
- this.resolutionScale = new VolumeParameter(defaultValue * factor);
71
- }
72
-
73
72
  // console.log(this.focusDistance.overrideState, this.focusDistance.value);
74
73
  // const depth = new DepthEffect({
75
74
  // inverted: true,
src/engine/engine_context.ts CHANGED
@@ -475,7 +475,7 @@
475
475
  this.renderer.shadowMap.type = PCFSoftShadowMap;
476
476
  this.renderer.setSize(this.domWidth, this.domHeight);
477
477
  this.renderer.outputColorSpace = SRGBColorSpace;
478
- this.renderer.toneMapping = AgXToneMapping;
478
+ // this.renderer.toneMapping = AgXToneMapping;
479
479
  this.lodsManager.setRenderer(this.renderer);
480
480
 
481
481
  this.input.bindEvents();
src/engine/engine_tonemapping.ts CHANGED
@@ -69,7 +69,7 @@
69
69
  vec3 x2 = x * x;
70
70
  vec3 x4 = x2 * x2;
71
71
 
72
- return + 15.5 * x4 * x2
72
+ return + 15.5 * x4 * x2
73
73
  - 40.14 * x4 * x
74
74
  + 31.96 * x4
75
75
  - 6.868 * x2 * x
src/engine-components/postprocessing/Effects/Pixelation.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  }
12
12
 
13
13
  @serializable(VolumeParameter)
14
- granularity!: VolumeParameter;
14
+ readonly granularity: VolumeParameter = new VolumeParameter(10);
15
15
 
16
16
  onCreateEffect(): EffectProviderResult {
17
17
 
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -65,9 +65,10 @@
65
65
  this._localInstance = this.asset?.instantiateSynced({ parent: this.gameObject }, true) as Promise<IGameObject>;
66
66
  const instance = await this._localInstance;
67
67
  if (instance) {
68
- const pl = GameObject.getComponent(instance, PlayerState);
69
- if (pl) {
70
- pl.owner = this.context.connection.connectionId!;
68
+ const pl = GameObject.getComponentsInChildren(instance, PlayerState);
69
+ if (pl?.length) {
70
+ for (const state of pl)
71
+ state.owner = this.context.connection.connectionId!;
71
72
  this.onPlayerSpawned?.invoke(instance);
72
73
  }
73
74
  else {
src/engine-components/postprocessing/PostProcessingEffect.ts CHANGED
@@ -2,18 +2,23 @@
2
2
 
3
3
  import type { EditorModification, IEditorModification } from "../../engine/engine_editor-sync.js";
4
4
  import { serializable } from "../../engine/engine_serialization.js";
5
- import type { ISerializable, SerializationContext } from "../../engine/engine_serialization_core.js";
6
5
  import { getParam } from "../../engine/engine_utils.js";
7
6
  import { Component } from "../Component.js";
8
- import { getPostProcessingManager,IPostProcessingManager } from "./utils.js";
7
+ import type { PostProcessingHandler } from "./PostProcessingHandler.js";
8
+ import { getPostProcessingManager, IPostProcessingManager } from "./utils.js";
9
9
  import { VolumeParameter } from "./VolumeParameter.js";
10
10
 
11
11
  const debug = getParam("debugpost");
12
12
 
13
13
  export declare type EffectProviderResult = Effect | Pass | Array<Effect | Pass>;
14
14
 
15
+ export declare type PostProcessingEffectContext = {
16
+ handler: PostProcessingHandler;
17
+ components: PostProcessingEffect[];
18
+ };
19
+
15
20
  export interface IEffectProvider {
16
- apply(): void | undefined | EffectProviderResult;
21
+ apply(context: PostProcessingEffectContext): void | undefined | EffectProviderResult;
17
22
  unapply(): void;
18
23
  }
19
24
 
@@ -43,13 +48,12 @@
43
48
  * registerCustomEffectType("Antialiasing", Antialiasing)
44
49
  * ```
45
50
  */
46
- export abstract class PostProcessingEffect extends Component implements IEffectProvider, ISerializable, IEditorModification {
51
+ export abstract class PostProcessingEffect extends Component implements IEffectProvider, IEditorModification {
47
52
 
48
53
  get isPostProcessingEffect() { return true; }
49
54
 
50
55
  constructor(params: any = undefined) {
51
56
  super();
52
- this.ensureVolumeParameters();
53
57
  if (params) {
54
58
  for (const key of Object.keys(params)) {
55
59
  const value = params[key];
@@ -59,7 +63,7 @@
59
63
  }
60
64
  // allow assigning values to properties that are not VolumeParameters
61
65
  // this is useful when effects are created in code
62
- else if(param !== undefined){
66
+ else if (param !== undefined) {
63
67
  this[key] = value;
64
68
  }
65
69
  }
@@ -68,11 +72,13 @@
68
72
 
69
73
  abstract get typeName(): string;
70
74
 
75
+ @serializable()
76
+ active: boolean = true;
77
+
71
78
  private _manager: IPostProcessingManager | null = null;
72
79
 
73
80
  onEnable(): void {
74
- this._manager = getPostProcessingManager(this);
75
- this._manager?.addEffect(this);
81
+ this.onEffectEnabled();
76
82
  // Dont override the serialized value by enabling (we could also just disable this component / map enabled to active)
77
83
  if (this.__internalDidAwakeAndStart)
78
84
  this.active = true;
@@ -83,24 +89,28 @@
83
89
  this.active = false;
84
90
  }
85
91
 
86
- @serializable()
87
- active: boolean = true;
92
+ protected onEffectEnabled(manager?: IPostProcessingManager) {
93
+ if (manager && manager.isPostProcessingManager === true) this._manager = manager;
94
+ else if (!this._manager) this._manager = getPostProcessingManager(this);
95
+ this._manager?.addEffect(this);
96
+ }
88
97
 
89
98
  /** override to initialize bindings on parameters */
90
- init() {
99
+ init() { }
91
100
 
92
- }
93
-
94
101
  /** previously created effect (if any) */
95
102
  private _result: void | undefined | EffectProviderResult;
103
+ private _postprocessingContext: PostProcessingEffectContext | null = null;
104
+ protected get postprocessingContext() { return this._postprocessingContext; }
96
105
 
97
-
98
106
  /** Apply post settings. Make sure to call super.apply() if you also create an effect */
99
- apply(): void | undefined | EffectProviderResult {
100
- this.ensureVolumeParameters();
107
+ apply(ctx: PostProcessingEffectContext): void | undefined | EffectProviderResult {
108
+ this._postprocessingContext = ctx;
101
109
  if (!this._result) {
110
+ this.initParameters();
102
111
  this._result = this.onCreateEffect?.call(this);
103
112
  }
113
+ // TODO: calling this twice because otherwise the Postprocessing sample doesnt look correct. Need to investigate which effect is causing this (init parameters should be refactored either way https://linear.app/needle/issue/NE-5182)
104
114
  if (this._result) {
105
115
  this.initParameters();
106
116
  }
@@ -137,25 +147,6 @@
137
147
  }
138
148
  }
139
149
 
140
- onAfterDeserialize(data: any, _context: SerializationContext): void {
141
- // When using additional effects and parameters exported from the editor are not in the volume parameter format
142
- if (typeof data === "object") {
143
- const types = this["$serializedTypes"];
144
- if (types) {
145
- for (const fieldName of Object.keys(types)) {
146
- const type = types[fieldName];
147
- if (type === VolumeParameter) {
148
- const value = data[fieldName];
149
- if (value !== undefined) {
150
- const parameter = this[fieldName];
151
- parameter.value = value;
152
- }
153
- }
154
- }
155
- }
156
- }
157
- }
158
-
159
150
  // TODO this is currently not used for post processing effects that are part of Volume stacks,
160
151
  // since these handle that already.
161
152
  onEditorModification(modification: EditorModification): void | boolean | undefined {
@@ -168,23 +159,4 @@
168
159
  }
169
160
  }
170
161
 
171
- private _didCreateVolumeParameters: boolean = false;
172
- /** Creates volume parameter fields that have not been initialized yet */
173
- protected ensureVolumeParameters() {
174
- if (this._didCreateVolumeParameters) return;
175
- this._didCreateVolumeParameters = true;
176
-
177
- const types = this["$serializedTypes"];
178
- if (types) {
179
- for (const fieldName of Object.keys(types)) {
180
- const type = types[fieldName];
181
- if (type === VolumeParameter) {
182
- const parameter = this[fieldName];
183
- if (!parameter) {
184
- this[fieldName] = new VolumeParameter();
185
- }
186
- }
187
- }
188
- }
189
- }
190
162
  }
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { N8AOPostPass } from "n8ao";
2
- import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SMAAEffect, SSAOEffect, ToneMappingEffect, VignetteEffect } from "postprocessing";
3
- import { HalfFloatType } from "three";
2
+ import {
3
+ BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SMAAEffect, SSAOEffect,
4
+ ToneMappingEffect as _TonemappingEffect, VignetteEffect
5
+ } from "postprocessing";
6
+ import { HalfFloatType, NoToneMapping } from "three";
4
7
 
5
8
  import { showBalloonWarning } from "../../engine/debug/index.js";
6
9
  import { Context } from "../../engine/engine_setup.js";
@@ -8,10 +11,8 @@
8
11
  import { getParam, isMobileDevice } from "../../engine/engine_utils.js";
9
12
  import { Camera } from "../Camera.js";
10
13
  import { Antialiasing } from "./Effects/Antialiasing.js";
11
- import { ColorAdjustments } from "./Effects/ColorAdjustments.js";
12
14
  import { SharpeningEffect } from "./Effects/Sharpening.js";
13
- import { ToneMapping } from "./Effects/Tonemapping.js";
14
- import { PostProcessingEffect } from "./PostProcessingEffect.js";
15
+ import { PostProcessingEffect, PostProcessingEffectContext } from "./PostProcessingEffect.js";
15
16
 
16
17
  const debug = getParam("debugpost");
17
18
 
@@ -31,7 +32,7 @@
31
32
  return this._isActive;
32
33
  }
33
34
 
34
- get composer() {
35
+ get composer() {
35
36
  return this._composer;
36
37
  }
37
38
 
@@ -79,8 +80,6 @@
79
80
  this._composer = null;
80
81
  }
81
82
 
82
- private tempColorAdjustments: ColorAdjustments | null = null;
83
-
84
83
  private onApply(context: Context, components: PostProcessingEffect[]) {
85
84
 
86
85
  if (!components) return;
@@ -95,17 +94,14 @@
95
94
  // const effects: Array<Effect | Pass> = [];
96
95
  this._effects.length = 0;
97
96
 
98
- // Tonemapping is currently applied from the ColorAdjustments effect, so we need to check if that is present,
99
- // and add a default ColorAdjustments effect if not.
100
- const haveTonemappingComponent = components.some(c => c instanceof ToneMapping);
101
- const haveColorAdjustmentsComponent = components.some(c => c instanceof ColorAdjustments && c.active);
102
- if (haveTonemappingComponent && !haveColorAdjustmentsComponent) {
103
- if (debug) console.log("Adding a default ColorAdjustments component to apply tonemapping.", ...components);
104
- if (this.tempColorAdjustments === null) this.tempColorAdjustments = new ColorAdjustments();
105
- this._lastVolumeComponents.push(this.tempColorAdjustments);
97
+
98
+ // TODO: if an effect is added or removed during the loop this might not be correct anymore
99
+ const ctx: PostProcessingEffectContext = {
100
+ handler: this,
101
+ components: this._lastVolumeComponents,
106
102
  }
107
-
108
- for (const component of this._lastVolumeComponents) {
103
+ for (let i = 0; i < this._lastVolumeComponents.length; i++) {
104
+ const component = this._lastVolumeComponents[i];
109
105
  //@ts-ignore
110
106
  component.context = context;
111
107
  if (component.apply) {
@@ -115,7 +111,7 @@
115
111
  return;
116
112
  }
117
113
  // apply or collect effects
118
- const res = component.apply();
114
+ const res = component.apply(ctx);
119
115
  if (!res) continue;
120
116
  if (Array.isArray(res)) {
121
117
  this._effects.push(...res);
@@ -129,6 +125,14 @@
129
125
  }
130
126
  }
131
127
 
128
+ // Ensure that we have a tonemapping effect if the renderer is set to use a tone mapping
129
+ if (this.context.renderer.toneMapping != NoToneMapping) {
130
+ if (!this._effects.find(e => e instanceof _TonemappingEffect)) {
131
+ const tonemapping = new _TonemappingEffect();
132
+ this._effects.push(tonemapping);
133
+ }
134
+ }
135
+
132
136
  this.applyEffects(context);
133
137
  }
134
138
 
@@ -143,7 +147,7 @@
143
147
  const renderer = context.renderer;
144
148
  const scene = context.scene;
145
149
  const cam = camera.cam;
146
-
150
+
147
151
  // Store the auto clear setting because the postprocessing composer just disables it
148
152
  // and when we disable postprocessing we want to restore the original setting
149
153
  // https://github.com/pmndrs/postprocessing/blob/271944b74b543a5b743a62803a167b60cc6bb4ee/src/core/EffectComposer.js#L230C12-L230C12
@@ -159,6 +163,9 @@
159
163
  multisampling: Math.min(isMobileDevice() ? 4 : 8, maxSamples),
160
164
  });
161
165
  }
166
+ if (context.composer && context.composer !== this._composer) {
167
+ 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.");
168
+ }
162
169
  context.composer = this._composer;
163
170
  const composer = context.composer;
164
171
  composer.setMainCamera(cam);
@@ -177,44 +184,44 @@
177
184
 
178
185
  const automaticEffectsOrdering = true;
179
186
  if (automaticEffectsOrdering) {
180
- try {
181
- this.orderEffects();
187
+ try {
188
+ this.orderEffects();
182
189
 
183
- const effects: Array<Effect> = [];
184
-
185
- for (const ef of effectsOrPasses) {
186
- if (ef instanceof Effect)
187
- effects.push(ef as Effect);
188
- else if (ef instanceof Pass) {
190
+ const effects: Array<Effect> = [];
191
+
192
+ for (const ef of effectsOrPasses) {
193
+ if (ef instanceof Effect)
194
+ effects.push(ef as Effect);
195
+ else if (ef instanceof Pass) {
196
+ const pass = new EffectPass(cam, ...effects);
197
+ pass.mainScene = scene;
198
+ pass.name = effects.map(e => e.constructor.name).join(", ");
199
+ pass.enabled = true;
200
+ // composer.addPass(pass);
201
+ effects.length = 0;
202
+ composer.addPass(ef as Pass);
203
+ }
204
+ else {
205
+ // seems some effects are not correctly typed, but three can deal with them,
206
+ // so we might need to just pass them through
207
+ // composer.addPass(ef);
208
+ }
209
+ }
210
+
211
+ // create and apply uber pass
212
+ if (effects.length > 0) {
189
213
  const pass = new EffectPass(cam, ...effects);
214
+ pass.name = effects.map(e => e.name).join(" ");
190
215
  pass.mainScene = scene;
191
- pass.name = effects.map(e => e.constructor.name).join(", ");
192
216
  pass.enabled = true;
193
- // composer.addPass(pass);
194
- effects.length = 0;
195
- composer.addPass(ef as Pass);
217
+ composer.addPass(pass);
196
218
  }
197
- else {
198
- // seems some effects are not correctly typed, but three can deal with them,
199
- // so we might need to just pass them through
200
- // composer.addPass(ef);
201
- }
202
219
  }
203
-
204
- // create and apply uber pass
205
- if (effects.length > 0) {
206
- const pass = new EffectPass(cam, ...effects);
207
- pass.name = effects.map(e => e.name).join(" ");
208
- pass.mainScene = scene;
209
- pass.enabled = true;
210
- composer.addPass(pass);
220
+ catch (e) {
221
+ console.error("Error while applying postprocessing effects", e);
222
+ composer.removeAllPasses();
211
223
  }
212
224
  }
213
- catch(e) {
214
- console.error("Error while applying postprocessing effects", e);
215
- composer.removeAllPasses();
216
- }
217
- }
218
225
  else {
219
226
  for (const ef of effectsOrPasses) {
220
227
  if (ef instanceof Effect)
@@ -237,10 +244,19 @@
237
244
  // TODO: enforce correct order of effects (e.g. DOF before Bloom)
238
245
  const effects = this._effects;
239
246
  effects.sort((a, b) => {
240
- const aidx = effectsOrder.indexOf(a.constructor as any);
241
- const bidx = effectsOrder.indexOf(b.constructor as any);
247
+ // we use find index here because sometimes constructor names are prefixed with `_`
248
+ // 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)
249
+ const aidx = effectsOrder.findIndex(e => a.constructor.name.endsWith(e.name));
250
+ const bidx = effectsOrder.findIndex(e => b.constructor.name.endsWith(e.name));
242
251
  // Unknown effects should be rendered first
243
- if (aidx < 0 && bidx < 0) return -1;
252
+ if (aidx < 0) {
253
+ if (debug) console.warn("Unknown effect found: ", a.constructor.name);
254
+ return -1;
255
+ }
256
+ else if (bidx < 0) {
257
+ if (debug) console.warn("Unknown effect found: ", b.constructor.name);
258
+ return 1;
259
+ }
244
260
  if (aidx < 0) return 1;
245
261
  if (bidx < 0) return -1;
246
262
  return aidx - bidx;
@@ -268,10 +284,10 @@
268
284
  BloomEffect,
269
285
  SelectiveBloomEffect,
270
286
  VignetteEffect,
271
- ToneMappingEffect,
287
+ PixelationEffect,
288
+ SharpeningEffect,
289
+ _TonemappingEffect,
272
290
  HueSaturationEffect,
273
291
  BrightnessContrastEffect,
274
- PixelationEffect,
275
- SharpeningEffect,
276
- Antialiasing
292
+ Antialiasing,
277
293
  ];
src/engine/codegen/register_types.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint.js";
34
34
  import { BehaviorExtension } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
35
35
  import { BehaviorModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
36
- import { Bloom } from "../../engine-components/postprocessing/Effects/Bloom.js";
36
+ import { BloomEffect } from "../../engine-components/postprocessing/Effects/BloomEffect.js";
37
37
  import { BoxCollider } from "../../engine-components/Collider.js";
38
38
  import { BoxGizmo } from "../../engine-components/Gizmos.js";
39
39
  import { BoxHelperComponent } from "../../engine-components/BoxHelperComponent.js";
@@ -186,7 +186,7 @@
186
186
  import { TextExtension } from "../../engine-components/export/usdz/extensions/USDZText.js";
187
187
  import { TextureSheetAnimationModule } from "../../engine-components/ParticleSystemModules.js";
188
188
  import { TiltShiftEffect } from "../../engine-components/postprocessing/Effects/TiltShiftEffect.js";
189
- import { ToneMapping } from "../../engine-components/postprocessing/Effects/Tonemapping.js";
189
+ import { ToneMappingEffect } from "../../engine-components/postprocessing/Effects/Tonemapping.js";
190
190
  import { TrailModule } from "../../engine-components/ParticleSystemModules.js";
191
191
  import { TransformData } from "../../engine-components/export/usdz/extensions/Animation.js";
192
192
  import { TransformGizmo } from "../../engine-components/TransformGizmo.js";
@@ -254,7 +254,7 @@
254
254
  TypeStore.add("BasicIKConstraint", BasicIKConstraint);
255
255
  TypeStore.add("BehaviorExtension", BehaviorExtension);
256
256
  TypeStore.add("BehaviorModel", BehaviorModel);
257
- TypeStore.add("Bloom", Bloom);
257
+ TypeStore.add("BloomEffect", BloomEffect);
258
258
  TypeStore.add("BoxCollider", BoxCollider);
259
259
  TypeStore.add("BoxGizmo", BoxGizmo);
260
260
  TypeStore.add("BoxHelperComponent", BoxHelperComponent);
@@ -407,7 +407,7 @@
407
407
  TypeStore.add("TextExtension", TextExtension);
408
408
  TypeStore.add("TextureSheetAnimationModule", TextureSheetAnimationModule);
409
409
  TypeStore.add("TiltShiftEffect", TiltShiftEffect);
410
- TypeStore.add("ToneMapping", ToneMapping);
410
+ TypeStore.add("ToneMappingEffect", ToneMappingEffect);
411
411
  TypeStore.add("TrailModule", TrailModule);
412
412
  TypeStore.add("TransformData", TransformData);
413
413
  TypeStore.add("TransformGizmo", TransformGizmo);
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion.ts CHANGED
@@ -14,19 +14,19 @@
14
14
  }
15
15
 
16
16
  @serializable(VolumeParameter)
17
- intensity!: VolumeParameter;
17
+ readonly intensity: VolumeParameter = new VolumeParameter(2);
18
18
 
19
19
  @serializable(VolumeParameter)
20
- falloff!: VolumeParameter;
20
+ readonly falloff: VolumeParameter = new VolumeParameter(1);
21
21
 
22
22
  @serializable(VolumeParameter)
23
- samples!: VolumeParameter;
23
+ readonly samples: VolumeParameter = new VolumeParameter(9);
24
24
 
25
25
  @serializable(VolumeParameter)
26
- color!: VolumeParameter;
26
+ readonly color: VolumeParameter = new VolumeParameter(new Color(0, 0, 0));
27
27
 
28
28
  @serializable(VolumeParameter)
29
- luminanceInfluence!: VolumeParameter;
29
+ readonly luminanceInfluence: VolumeParameter = new VolumeParameter(.7);
30
30
 
31
31
  onBeforeRender() {
32
32
  if (this._ssao && this.context.mainCamera instanceof PerspectiveCamera) {
src/engine-components/postprocessing/Effects/Sharpening.ts CHANGED
@@ -56,35 +56,41 @@
56
56
  `
57
57
 
58
58
  const frag = `
59
- uniform sampler2D tDiffuse;
60
- uniform float amount;
61
- uniform float threshold;
62
- uniform float radius;
59
+ uniform sampler2D tDiffuse;
60
+ uniform float amount;
61
+ uniform float radius;
63
62
 
64
- void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
63
+ void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
65
64
  float tx = 1.0 / resolution.x;
66
65
  float ty = 1.0 / resolution.y;
67
66
  vec2 texelSize = vec2(tx, ty);
68
67
 
69
- vec4 color = texture2D(tDiffuse, uv);
70
68
  vec4 blurred = vec4(0.0);
71
69
  float total = 0.0;
70
+
72
71
  for (float x = -radius; x <= radius; x++) {
73
- for (float y = -radius; y <= radius; y++) {
74
- vec2 offset = vec2(x, y) * texelSize;
75
- vec4 diffuse = texture2D(tDiffuse, uv + offset);
76
- float weight = exp(-length(offset) * amount);
77
- blurred += diffuse * weight;
78
- total += weight;
79
- }
72
+ for (float y = -radius; y <= radius; y++) {
73
+ vec2 offset = vec2(x, y) * texelSize;
74
+ vec4 diffuse = texture2D(tDiffuse, uv + offset);
75
+ float weight = exp(-length(offset) * amount);
76
+ blurred += diffuse * weight;
77
+ total += weight;
78
+ }
80
79
  }
81
- blurred /= total;
80
+
81
+ if (total > 0.0) {
82
+ blurred /= total;
83
+ }
84
+
85
+ // Calculate the sharpened color using inputColor
82
86
  vec4 sharp = inputColor + (inputColor - blurred) * amount;
83
- // float luma = dot(inputColor.rgb, vec3(0.299, 0.587, 0.114));
84
- // float blend = smoothstep(threshold, 1.0, luma);
85
- outputColor = sharp; //mix(inputColor, sharp, blend);
86
-
87
+
88
+ // Ensure the sharp color does not go below 0 or above 1
89
+ sharp = clamp(sharp, 0.0, 1.0);
90
+
91
+ outputColor = sharp;
87
92
  }
93
+
88
94
  `
89
95
 
90
96
  class _SharpeningEffect extends Effect {
@@ -93,8 +99,8 @@
93
99
  vertexShader: vert,
94
100
  blendFunction: BlendFunction.NORMAL,
95
101
  uniforms: new Map<string, Uniform<any>>([
96
- ["amount", new Uniform(.8)],
97
- ["radius", new Uniform(.5)],
102
+ ["amount", new Uniform(1)],
103
+ ["radius", new Uniform(1)],
98
104
  // ["threshold", new Uniform(0)],
99
105
  ]),
100
106
  });
src/engine-components/postprocessing/Effects/TiltShiftEffect.ts CHANGED
@@ -36,8 +36,6 @@
36
36
 
37
37
  onCreateEffect(): EffectProviderResult | undefined {
38
38
 
39
- console.log(this);
40
-
41
39
  const effect = new TiltShift({
42
40
  kernelSize: KernelSize.VERY_LARGE,
43
41
  });
src/engine-components/postprocessing/Effects/Tonemapping.ts CHANGED
@@ -1,12 +1,17 @@
1
+ import { ToneMappingEffect as _TonemappingEffect, ToneMappingMode } from "postprocessing";
1
2
  import { ACESFilmicToneMapping, AgXToneMapping, LinearToneMapping, NeutralToneMapping, NoToneMapping, ReinhardToneMapping } from "three";
2
3
 
3
4
  import { serializable } from "../../../engine/engine_serialization.js";
4
- import { PostProcessingEffect } from "../PostProcessingEffect.js";
5
+ import { getParam } from "../../../engine/engine_utils.js";
6
+ import { EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
7
+ import { findPostProcessingManager } from "../utils.js";
5
8
  import { VolumeParameter } from "../VolumeParameter.js";
6
9
  import { registerCustomEffectType } from "../VolumeProfile.js";
7
10
 
11
+ const debug = getParam("debugpost");
8
12
 
9
- export enum TonemappingMode {
13
+
14
+ enum NEToneMappingMode {
10
15
  None = 0,
11
16
  Neutral = 1, // Neutral tonemapper, close to Reinhard
12
17
  ACES = 2, // ACES Filmic reference tonemapper (custom approximation)
@@ -14,52 +19,119 @@
14
19
  KhronosNeutral = 4, // PBR Neural tonemapper
15
20
  }
16
21
 
17
- export class ToneMapping extends PostProcessingEffect {
22
+ type NEToneMappingModeNames = keyof typeof NEToneMappingMode;
18
23
 
24
+
25
+ function toThreeToneMapping(mode: NEToneMappingMode | undefined) {
26
+ switch (mode) {
27
+ case NEToneMappingMode.None:
28
+ return LinearToneMapping;
29
+ case NEToneMappingMode.Neutral:
30
+ return ReinhardToneMapping;
31
+ case NEToneMappingMode.ACES:
32
+ return ACESFilmicToneMapping;
33
+ case NEToneMappingMode.AgX:
34
+ return AgXToneMapping;
35
+ case NEToneMappingMode.KhronosNeutral:
36
+ return NeutralToneMapping;
37
+ default:
38
+ return NeutralToneMapping;
39
+ }
40
+ }
41
+
42
+ function threeToNeToneMapping(mode: number | undefined): NEToneMappingMode {
43
+ switch (mode) {
44
+ case LinearToneMapping: return NEToneMappingMode.None;
45
+ case ACESFilmicToneMapping: return NEToneMappingMode.ACES;
46
+ case AgXToneMapping: return NEToneMappingMode.AgX;
47
+ case NeutralToneMapping: return NEToneMappingMode.Neutral;
48
+ case ReinhardToneMapping: return NEToneMappingMode.Neutral;
49
+ default: return NEToneMappingMode.None;
50
+ }
51
+
52
+ }
53
+
54
+
55
+ function threeToneMappingToEffectMode(mode: number | undefined): ToneMappingMode {
56
+ switch (mode) {
57
+ case LinearToneMapping: return ToneMappingMode.LINEAR;
58
+ case ACESFilmicToneMapping: return ToneMappingMode.ACES_FILMIC;
59
+ case AgXToneMapping: return ToneMappingMode.AGX;
60
+ case NeutralToneMapping: return ToneMappingMode.NEUTRAL;
61
+ case ReinhardToneMapping: return ToneMappingMode.REINHARD;
62
+ default: return ToneMappingMode.LINEAR;
63
+ }
64
+ }
65
+
66
+
67
+ export class ToneMappingEffect extends PostProcessingEffect {
68
+
19
69
  get typeName() {
20
70
  return "ToneMapping";
21
71
  }
22
72
 
23
73
  @serializable(VolumeParameter)
24
- mode: VolumeParameter | undefined;
74
+ readonly mode: VolumeParameter = new VolumeParameter(undefined);
25
75
 
76
+ setMode(mode: NEToneMappingModeNames) {
77
+ const enumValue = NEToneMappingMode[mode as NEToneMappingModeNames];
78
+ if (enumValue === undefined) {
79
+ console.error("Invalid ToneMapping mode", mode);
80
+ return this;
81
+ }
82
+ this.mode.value = enumValue;
83
+ return this;
84
+ }
85
+
26
86
  get isToneMapping() { return true; }
27
87
 
28
- init(): void {
29
- if (!this.mode) this.mode = new VolumeParameter(NoToneMapping);
30
- this.mode.defaultValue = NoToneMapping;
88
+ onEffectEnabled(): void {
89
+ // Tonemapping works with and without a postprocessing manager.
90
+ // If there's no manager already in the scene we don't need to create one because tonemapping can also be applied without a postprocessing pass
91
+ const ppmanager = findPostProcessingManager(this);
92
+ if (!ppmanager) return;
93
+ super.onEffectEnabled(ppmanager);
31
94
  }
32
95
 
33
- apply() {
34
- if (!this.mode) this.init();
35
- this.mode!.onValueChanged = this._apply.bind(this);
36
- this._apply(this.mode!.value)
37
- }
96
+ onCreateEffect(): EffectProviderResult | undefined {
97
+ // TODO: this should be done in the PostProcessingHandler
98
+ if (this.postprocessingContext) {
99
+ for (const other of this.postprocessingContext.components) {
100
+ // If we're the first tonemapping effect it's all good
101
+ if (other === this) break;
102
+ // If another tonemapping effect is found, warn the user
103
+ if (other != this && other instanceof ToneMappingEffect) {
104
+ console.warn("Multiple tonemapping effects found in the same postprocessing stack: Please check your scene setup.", { activeEffect: other, ignoredEffect: this });
105
+ return undefined;
106
+ }
107
+ }
108
+ }
38
109
 
39
- private _apply(v: TonemappingMode) {
40
- // The renderer tonemapping mode is used when NO volume is in effect.
41
- // It does not have an effect when a composer exists, and then tonemapping is applied as posteffect
42
- // from ColodAdjustments.ts
43
- this.context.renderer.toneMapping = this.getThreeToneMapping(v);
110
+ // ensure the effect tonemapping value is initialized
111
+ if (this.mode.isInitialized == false) {
112
+ const init = threeToNeToneMapping(this.context.renderer.toneMapping);
113
+ this.mode.initialize(init);
114
+ }
115
+
116
+ const threeMode = toThreeToneMapping(this.mode.value);
117
+ const tonemapping = new _TonemappingEffect({
118
+ mode: threeToneMappingToEffectMode(threeMode),
119
+ });
120
+ this.mode.onValueChanged = (newValue) => {
121
+ const threeMode = toThreeToneMapping(newValue);
122
+ tonemapping.mode = threeToneMappingToEffectMode(threeMode);
123
+ if (debug) console.log("ToneMapping mode changed to", newValue, NEToneMappingMode[threeMode], ToneMappingMode[tonemapping.mode]);
124
+ };
125
+ if (debug) console.log("Use ToneMapping", this.context.renderer.toneMapping, this.mode.value, NEToneMappingMode[threeMode], ToneMappingMode[tonemapping.mode]);
126
+ return tonemapping;
44
127
  }
45
128
 
46
- getThreeToneMapping(mode: TonemappingMode | undefined) {
47
- switch (mode) {
48
- case TonemappingMode.None:
49
- return LinearToneMapping;
50
- case TonemappingMode.Neutral:
51
- return ReinhardToneMapping;
52
- case TonemappingMode.ACES:
53
- return ACESFilmicToneMapping;
54
- case TonemappingMode.AgX:
55
- return AgXToneMapping;
56
- case TonemappingMode.KhronosNeutral:
57
- return NeutralToneMapping;
58
- default:
59
- return NeutralToneMapping;
60
- }
129
+
130
+ onBeforeRender(): void {
131
+ this.context.renderer.toneMapping = toThreeToneMapping(this.mode.value);
61
132
  }
62
133
 
134
+
63
135
  }
64
136
 
65
- registerCustomEffectType("Tonemapping", ToneMapping);
137
+ registerCustomEffectType("Tonemapping", ToneMappingEffect);
src/engine-components/postprocessing/utils.ts CHANGED
@@ -22,18 +22,21 @@
22
22
  PostprocessingManagerType = type;
23
23
  }
24
24
 
25
- export function getPostProcessingManager(effect: PostProcessingEffect): IPostProcessingManager | null {
26
- let manager: IPostProcessingManager | null = null;
25
+ export function findPostProcessingManager(effect: PostProcessingEffect): IPostProcessingManager | null {
27
26
  let obj = effect.gameObject as Object3D | null;
28
27
  while (obj) {
29
28
  for (const comp of foreachComponentEnumerator(obj)) {
30
29
  if ((comp as unknown as IPostProcessingManager).isPostProcessingManager === true) {
31
- manager = comp as unknown as IPostProcessingManager;
32
- break;
30
+ return comp as unknown as IPostProcessingManager;
33
31
  }
34
32
  }
35
33
  obj = obj.parent;
36
34
  }
35
+ return null;
36
+ }
37
+
38
+ export function getPostProcessingManager(effect: PostProcessingEffect): IPostProcessingManager | null {
39
+ let manager: IPostProcessingManager | null = findPostProcessingManager(effect);
37
40
  if (!manager) {
38
41
  if (PostprocessingManagerType) {
39
42
  if (debug)
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -99,7 +99,7 @@
99
99
  /** @internal */
100
100
  awake() {
101
101
  if (debug) {
102
- console.log(this);
102
+ console.log("PostprocessingManager Awake", this);
103
103
  console.log("Press P to toggle post processing");
104
104
  window.addEventListener("keydown", (e) => {
105
105
  if (e.key === "p") {
@@ -159,7 +159,7 @@
159
159
  private _isDirty: boolean = false;
160
160
 
161
161
  private apply() {
162
- if (debug) console.log("Apply PostProcessing", this, this.context.mainCamera?.name);
162
+ if (debug) console.log("Apply PostProcessing " + this.name);
163
163
 
164
164
  if (isDevEnvironment()) {
165
165
  if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) {
@@ -198,7 +198,7 @@
198
198
  }
199
199
 
200
200
  private unapply() {
201
- if (debug) console.log("Unapply PostProcessing", this);
201
+ if (debug) console.log("Unapply PostProcessing " + this.name);
202
202
  this._postprocessing?.unapply();
203
203
  }
204
204
 
src/engine-components/postprocessing/VolumeParameter.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { serializable } from "../../engine/engine_serialization.js";
1
+ import { deserializeObject, serializable,SerializationContext } from "../../engine/engine_serialization.js";
2
+ import { TypeSerializer } from "../../engine/engine_serialization_core.js";
2
3
  import { getParam } from "../../engine/engine_utils.js";
3
4
 
5
+
6
+
4
7
  export declare type VolumeParameterChangedEvent = (newValue: any, oldValue: any, parameter: VolumeParameter) => void;
5
8
  export declare type VolumeParameterValueProcessor = (value: any) => any;
6
9
 
@@ -8,12 +11,25 @@
8
11
 
9
12
  export class VolumeParameter {
10
13
 
14
+ readonly isVolumeParameter = true;
15
+
11
16
  constructor(value?: any) {
12
- this._value = value;
13
- this._defaultValue = value;
14
- this._valueRaw = value;
17
+ if (value !== undefined)
18
+ this.initialize(value);
15
19
  }
16
20
 
21
+ private _isInitialized: boolean = false;
22
+ get isInitialized() { return this._isInitialized; }
23
+
24
+ initialize(value?: any) {
25
+ if (value !== undefined) {
26
+ this._value = value;
27
+ this._defaultValue = value;
28
+ this._valueRaw = value;
29
+ this._isInitialized = true;
30
+ }
31
+ }
32
+
17
33
  @serializable()
18
34
  get overrideState(): boolean {
19
35
  return this._active;
@@ -73,8 +89,6 @@
73
89
  }
74
90
  else hasChanged = false;
75
91
  }
76
- if (hasChanged)
77
- console.log("VolumeParameter: value changed from", oldValue, "to", val);
78
92
  }
79
93
 
80
94
  if (!this._active && this._defaultValue !== undefined) {
@@ -109,3 +123,38 @@
109
123
  return true;
110
124
  }
111
125
  }
126
+
127
+
128
+
129
+ class VolumeParameterSerializer extends TypeSerializer {
130
+ constructor() {
131
+ super([VolumeParameter]);
132
+ }
133
+ onSerialize(_data: any, _context: SerializationContext) {
134
+ }
135
+ onDeserialize(data: { value: any, overrideState: boolean }, context: SerializationContext) {
136
+ const target = context.target;
137
+ const name = context.path;
138
+
139
+ let parameter: VolumeParameter | undefined;
140
+ if (target && name) {
141
+ parameter = target[name];
142
+ }
143
+
144
+ if (!(typeof parameter === "object") || (typeof parameter === "object" && (parameter as VolumeParameter).isVolumeParameter !== true)) {
145
+ parameter = new VolumeParameter();
146
+ }
147
+
148
+ if (typeof data === "object" && "value" in data) {
149
+ const value = data.value;
150
+ parameter.value = value;
151
+ parameter.overrideState = data.overrideState;
152
+ }
153
+ else {
154
+ parameter.value = data;
155
+ }
156
+
157
+ return parameter;
158
+ }
159
+ }
160
+ new VolumeParameterSerializer();
src/engine-components/webxr/WebXR.ts CHANGED
@@ -269,7 +269,8 @@
269
269
  private onAvatarSpawned = (instance: GameObject) => {
270
270
  // spawned webxr avatars must have a avatar component
271
271
  if (debug) console.log("WebXR.onAvatarSpawned", instance);
272
- GameObject.getOrAddComponent(instance, Avatar);
272
+ let avatar = GameObject.getComponentInChildren(instance, Avatar);
273
+ avatar ??= GameObject.addComponent(instance, Avatar)!;
273
274
  };
274
275
 
275
276
 
src/engine-components/postprocessing/Effects/BloomEffect.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { BlendFunction, BloomEffect as _BloomEffect, SelectiveBloomEffect } from "postprocessing";
2
+ import { MathUtils } from "three";
3
+
4
+ import { serializable } from "../../../engine/engine_serialization.js";
5
+ import { PostProcessingEffect } from "../PostProcessingEffect.js";
6
+ import { VolumeParameter } from "../VolumeParameter.js";
7
+ import { registerCustomEffectType } from "../VolumeProfile.js";
8
+
9
+ /**
10
+ * Bloom can be used to make bright areas in the scene glow.
11
+ * @link Sample https://engine.needle.tools/samples/postprocessing
12
+ * @example
13
+ * ```typescript
14
+ * const bloom = new Bloom();
15
+ * bloom.intensity.value = 1.5;
16
+ * bloom.threshold.value = 0.5;
17
+ * bloom.scatter.value = 0.5;
18
+ * volume.add(bloom);
19
+ * ```
20
+ */
21
+ export class BloomEffect extends PostProcessingEffect {
22
+
23
+ /** Whether to use selective bloom by default */
24
+ static useSelectiveBloom = false;
25
+
26
+ get typeName() {
27
+ return "Bloom";
28
+ }
29
+
30
+ /**
31
+ * The bloom threshold controls at what brightness level the bloom effect will be applied.
32
+ * A higher value means the bloom will be applied to brighter areas or lights only
33
+ * @default 0.9
34
+ */
35
+ @serializable(VolumeParameter)
36
+ readonly threshold: VolumeParameter = new VolumeParameter(.9);
37
+
38
+ /**
39
+ * Intensity of the bloom effect. A higher value will increase the intensity of the bloom effect.
40
+ * @default 1
41
+ */
42
+ @serializable(VolumeParameter)
43
+ readonly intensity: VolumeParameter = new VolumeParameter(1);
44
+
45
+ /**
46
+ * Scatter value. The higher the value, the more the bloom will scatter.
47
+ * @default 0.3
48
+ */
49
+ @serializable(VolumeParameter)
50
+ readonly scatter: VolumeParameter = new VolumeParameter(.3);
51
+
52
+ /**
53
+ * Set to true to use selective bloom when the effect gets created.
54
+ * @default false
55
+ */
56
+ selectiveBloom?: boolean;
57
+
58
+ init() {
59
+ this.threshold.valueProcessor = (v: number) => v;
60
+ this.intensity.valueProcessor = (v: number) => v;
61
+ this.scatter.valueProcessor = (v: number) => v;
62
+ }
63
+
64
+ onCreateEffect() {
65
+ let bloom: _BloomEffect;
66
+
67
+ if (this.selectiveBloom == undefined) {
68
+ this.selectiveBloom = BloomEffect.useSelectiveBloom;
69
+ }
70
+
71
+ if (this.selectiveBloom) {
72
+ // https://github.com/pmndrs/postprocessing/blob/64d2829f014cfec97a46bf3c109f3abc55af0715/demo/src/demos/BloomDemo.js#L265
73
+ const selectiveBloom = bloom = new SelectiveBloomEffect(this.context.scene, this.context.mainCamera!, {
74
+ blendFunction: BlendFunction.ADD,
75
+ mipmapBlur: true,
76
+ luminanceThreshold: this.threshold.value,
77
+ luminanceSmoothing: this.scatter.value,
78
+ radius: 0.85, // default value
79
+ intensity: this.intensity.value,
80
+ });
81
+ selectiveBloom.inverted = true;
82
+ }
83
+ else {
84
+ bloom = new _BloomEffect({
85
+ blendFunction: BlendFunction.ADD,
86
+ mipmapBlur: true,
87
+ luminanceThreshold: this.threshold.value,
88
+ luminanceSmoothing: this.scatter.value,
89
+ radius: 0.85, // default value
90
+ intensity: this.intensity.value,
91
+ });
92
+ }
93
+
94
+ this.intensity.onValueChanged = newValue => {
95
+ bloom!.intensity = newValue;
96
+ };
97
+ this.threshold.onValueChanged = newValue => {
98
+ // for some reason the threshold needs to be gamma-corrected
99
+ bloom!.luminanceMaterial.threshold = Math.pow(newValue, 2.2);
100
+ };
101
+ this.scatter.onValueChanged = newValue => {
102
+ bloom!.luminancePass.enabled = true;
103
+ bloom!.luminanceMaterial.smoothing = newValue;
104
+ if (bloom["mipmapBlurPass"])
105
+ // heuristic so it looks similar to "scatter" in other engines
106
+ bloom!["mipmapBlurPass"].radius = MathUtils.lerp(0.1, 0.9, newValue);
107
+ };
108
+
109
+ return bloom;
110
+ }
111
+
112
+ }
113
+ registerCustomEffectType("Bloom", BloomEffect);