@babylonjs/smart-filters 0.1.0-alpha

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 (138) hide show
  1. package/dist/IDisposable.d.ts +10 -0
  2. package/dist/IDisposable.d.ts.map +1 -0
  3. package/dist/IDisposable.js +2 -0
  4. package/dist/IDisposable.js.map +1 -0
  5. package/dist/blocks/aggregateBlock.d.ts +50 -0
  6. package/dist/blocks/aggregateBlock.d.ts.map +1 -0
  7. package/dist/blocks/aggregateBlock.js +103 -0
  8. package/dist/blocks/aggregateBlock.js.map +1 -0
  9. package/dist/blocks/baseBlock.d.ts +160 -0
  10. package/dist/blocks/baseBlock.d.ts.map +1 -0
  11. package/dist/blocks/baseBlock.js +256 -0
  12. package/dist/blocks/baseBlock.js.map +1 -0
  13. package/dist/blocks/copyBlock.d.ts +59 -0
  14. package/dist/blocks/copyBlock.d.ts.map +1 -0
  15. package/dist/blocks/copyBlock.js +84 -0
  16. package/dist/blocks/copyBlock.js.map +1 -0
  17. package/dist/blocks/disableableBlock.d.ts +30 -0
  18. package/dist/blocks/disableableBlock.d.ts.map +1 -0
  19. package/dist/blocks/disableableBlock.js +22 -0
  20. package/dist/blocks/disableableBlock.js.map +1 -0
  21. package/dist/blocks/inputBlock.d.ts +64 -0
  22. package/dist/blocks/inputBlock.d.ts.map +1 -0
  23. package/dist/blocks/inputBlock.js +74 -0
  24. package/dist/blocks/inputBlock.js.map +1 -0
  25. package/dist/blocks/outputBlock.d.ts +42 -0
  26. package/dist/blocks/outputBlock.d.ts.map +1 -0
  27. package/dist/blocks/outputBlock.js +74 -0
  28. package/dist/blocks/outputBlock.js.map +1 -0
  29. package/dist/blocks/shaderBlock.d.ts +68 -0
  30. package/dist/blocks/shaderBlock.d.ts.map +1 -0
  31. package/dist/blocks/shaderBlock.js +101 -0
  32. package/dist/blocks/shaderBlock.js.map +1 -0
  33. package/dist/command/command.d.ts +49 -0
  34. package/dist/command/command.d.ts.map +1 -0
  35. package/dist/command/command.js +15 -0
  36. package/dist/command/command.js.map +1 -0
  37. package/dist/command/commandBuffer.d.ts +40 -0
  38. package/dist/command/commandBuffer.d.ts.map +1 -0
  39. package/dist/command/commandBuffer.js +58 -0
  40. package/dist/command/commandBuffer.js.map +1 -0
  41. package/dist/command/commandBufferDebugger.d.ts +7 -0
  42. package/dist/command/commandBufferDebugger.d.ts.map +1 -0
  43. package/dist/command/commandBufferDebugger.js +12 -0
  44. package/dist/command/commandBufferDebugger.js.map +1 -0
  45. package/dist/connection/connectionPoint.d.ts +110 -0
  46. package/dist/connection/connectionPoint.d.ts.map +1 -0
  47. package/dist/connection/connectionPoint.js +153 -0
  48. package/dist/connection/connectionPoint.js.map +1 -0
  49. package/dist/connection/connectionPointCompatibilityState.d.ts +20 -0
  50. package/dist/connection/connectionPointCompatibilityState.d.ts.map +1 -0
  51. package/dist/connection/connectionPointCompatibilityState.js +32 -0
  52. package/dist/connection/connectionPointCompatibilityState.js.map +1 -0
  53. package/dist/connection/connectionPointDirection.d.ts +10 -0
  54. package/dist/connection/connectionPointDirection.d.ts.map +1 -0
  55. package/dist/connection/connectionPointDirection.js +11 -0
  56. package/dist/connection/connectionPointDirection.js.map +1 -0
  57. package/dist/connection/connectionPointType.d.ts +22 -0
  58. package/dist/connection/connectionPointType.d.ts.map +1 -0
  59. package/dist/connection/connectionPointType.js +17 -0
  60. package/dist/connection/connectionPointType.js.map +1 -0
  61. package/dist/connection/connectionPointWithDefault.d.ts +23 -0
  62. package/dist/connection/connectionPointWithDefault.d.ts.map +1 -0
  63. package/dist/connection/connectionPointWithDefault.js +19 -0
  64. package/dist/connection/connectionPointWithDefault.js.map +1 -0
  65. package/dist/index.d.ts +28 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +28 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/optimization/dependencyGraph.d.ts +31 -0
  70. package/dist/optimization/dependencyGraph.d.ts.map +1 -0
  71. package/dist/optimization/dependencyGraph.js +77 -0
  72. package/dist/optimization/dependencyGraph.js.map +1 -0
  73. package/dist/optimization/optimizedShaderBlock.d.ts +75 -0
  74. package/dist/optimization/optimizedShaderBlock.d.ts.map +1 -0
  75. package/dist/optimization/optimizedShaderBlock.js +105 -0
  76. package/dist/optimization/optimizedShaderBlock.js.map +1 -0
  77. package/dist/optimization/smartFilterOptimizer.d.ts +72 -0
  78. package/dist/optimization/smartFilterOptimizer.d.ts.map +1 -0
  79. package/dist/optimization/smartFilterOptimizer.js +482 -0
  80. package/dist/optimization/smartFilterOptimizer.js.map +1 -0
  81. package/dist/runtime/renderTargetGenerator.d.ts +35 -0
  82. package/dist/runtime/renderTargetGenerator.d.ts.map +1 -0
  83. package/dist/runtime/renderTargetGenerator.js +153 -0
  84. package/dist/runtime/renderTargetGenerator.js.map +1 -0
  85. package/dist/runtime/shaderRuntime.d.ts +95 -0
  86. package/dist/runtime/shaderRuntime.d.ts.map +1 -0
  87. package/dist/runtime/shaderRuntime.js +120 -0
  88. package/dist/runtime/shaderRuntime.js.map +1 -0
  89. package/dist/runtime/smartFilterRuntime.d.ts +69 -0
  90. package/dist/runtime/smartFilterRuntime.d.ts.map +1 -0
  91. package/dist/runtime/smartFilterRuntime.js +57 -0
  92. package/dist/runtime/smartFilterRuntime.js.map +1 -0
  93. package/dist/runtime/strongRef.d.ts +16 -0
  94. package/dist/runtime/strongRef.d.ts.map +1 -0
  95. package/dist/runtime/strongRef.js +9 -0
  96. package/dist/runtime/strongRef.js.map +1 -0
  97. package/dist/smartFilter.d.ts +100 -0
  98. package/dist/smartFilter.d.ts.map +1 -0
  99. package/dist/smartFilter.js +154 -0
  100. package/dist/smartFilter.js.map +1 -0
  101. package/dist/utils/shaderCodeUtils.d.ts +131 -0
  102. package/dist/utils/shaderCodeUtils.d.ts.map +1 -0
  103. package/dist/utils/shaderCodeUtils.js +115 -0
  104. package/dist/utils/shaderCodeUtils.js.map +1 -0
  105. package/dist/utils/textureLoaders.d.ts +14 -0
  106. package/dist/utils/textureLoaders.d.ts.map +1 -0
  107. package/dist/utils/textureLoaders.js +22 -0
  108. package/dist/utils/textureLoaders.js.map +1 -0
  109. package/license.md +21 -0
  110. package/package.json +47 -0
  111. package/readme.md +165 -0
  112. package/src/IDisposable.ts +9 -0
  113. package/src/blocks/aggregateBlock.ts +121 -0
  114. package/src/blocks/baseBlock.ts +341 -0
  115. package/src/blocks/copyBlock.ts +103 -0
  116. package/src/blocks/disableableBlock.ts +40 -0
  117. package/src/blocks/inputBlock.ts +114 -0
  118. package/src/blocks/outputBlock.ts +97 -0
  119. package/src/blocks/shaderBlock.ts +145 -0
  120. package/src/command/command.ts +60 -0
  121. package/src/command/commandBuffer.ts +71 -0
  122. package/src/command/commandBufferDebugger.ts +13 -0
  123. package/src/connection/connectionPoint.ts +212 -0
  124. package/src/connection/connectionPointCompatibilityState.ts +31 -0
  125. package/src/connection/connectionPointDirection.ts +9 -0
  126. package/src/connection/connectionPointType.ts +30 -0
  127. package/src/connection/connectionPointWithDefault.ts +35 -0
  128. package/src/index.ts +36 -0
  129. package/src/optimization/dependencyGraph.ts +94 -0
  130. package/src/optimization/optimizedShaderBlock.ts +133 -0
  131. package/src/optimization/smartFilterOptimizer.ts +706 -0
  132. package/src/runtime/renderTargetGenerator.ts +204 -0
  133. package/src/runtime/shaderRuntime.ts +155 -0
  134. package/src/runtime/smartFilterRuntime.ts +104 -0
  135. package/src/runtime/strongRef.ts +18 -0
  136. package/src/smartFilter.ts +227 -0
  137. package/src/utils/shaderCodeUtils.ts +242 -0
  138. package/src/utils/textureLoaders.ts +28 -0
package/readme.md ADDED
@@ -0,0 +1,165 @@
1
+ # Babylon.js Smart Filters - PREVIEW
2
+
3
+ # PREVIEW WARNING
4
+
5
+ This package is currently in preview form, and updates will likely include breaking changes. It is not yet intended to be used by other projects.
6
+
7
+ ## Core
8
+
9
+ A Smart Filter is a node based description of effects to apply on various inputs in order to create a visual output on a canvas element.
10
+
11
+ The main usage is for video processing and composition but it could be easily reused for picture edition or procedural creation.
12
+
13
+ ## How to install
14
+
15
+ This can be installed with `npm install @babylonjs/smart-filters`.
16
+
17
+ It requires the following peer dependencies:
18
+
19
+ - @babylonjs/core
20
+
21
+ ## How to use
22
+
23
+ The overall usage would look like:
24
+
25
+ ```typescript
26
+ // Create a filter
27
+ const smartFilter = new SmartFilter("Simplest");
28
+ const titleInput = new InputBlock(smartFilter, "logo", ConnectionPointType.Texture, logoTexture);
29
+ titleInput.output.connectTo(smartFilter.output);
30
+
31
+ // Create a runtime to display the filter
32
+ const engine = new ThinEngine(canvas, true);
33
+ const runtime = await filter.createRuntimeAsync(engine);
34
+
35
+ // Render one frame
36
+ runtime.render();
37
+ ```
38
+
39
+ The package has been marked as `sideEffects: false` so you can freely import from the index and know that tree shaking will remove any code you don't end up using.
40
+
41
+ ## How it works
42
+
43
+ The entire system has been split in two parts the filter and the runtime.
44
+
45
+ ### SmartFilter
46
+
47
+ The notion of a `SmartFilter` is a graph of blocks (all inheriting from `BaseBlock`) linked to each other through `ConnectionPoint`s.
48
+
49
+ During the initialization phase, the `SmartFilter` examines the graph and builds a list of commands to execute during each frame and stores them in a `CommandBuffer`:
50
+
51
+ - This keeps each frame render very fast by ensuring the work is branchless.
52
+ - Basically, every `BaseBlock` during its `initialize` step will register in the `CommandBuffer` only the work it has to execute according to its options.
53
+ - In the end the render loop it only needs to run and execute each command in the list.
54
+ - This also allows for easy injection of debug tools or logging in between each command without growing the minimum required code to run a filter.
55
+ - As discussed, the `Blocks` are responsible for "injecting" their command in the filter `CommandBuffer`.
56
+ - For example: the `ShaderBlock` highlights how convenient it can be to only switch once at init time between rendering to the main frame buffer (if linked to the output) or to an intermediate texture if in the middle of the chain.
57
+
58
+ Each block is responsible for exposing their inputs and outputs through `registerInput` and `registerOutput`:
59
+
60
+ - This creates and stores the related `ConnectionPoint`.
61
+ - All property values of a block must be updated only before `initialize` is called as the runtime won't reference it at render time.
62
+ - All values that need to be dynamically updated after `initialize` must be defined as connection points or 'StrongRef'.
63
+
64
+ The `ConnectionPoints` are:
65
+
66
+ - Strongly typed, allowing more type safety while creating `SmartFilter` by code, yet keeping enough flexibility to be understood efficiently at runtime.
67
+ - They are also responsible for their compatibility when linked to each other.
68
+
69
+ The Smart Filter is fully abstracted away from the runtime notion and does not even require a Babylon Engine to work with. It is only responsible to hold the "graph" of the filter or the "map" of all its blocks. A Filter only needs a name to be created:
70
+
71
+ ```typescript
72
+ const smartFilter = new SmartFilter("Simplest");
73
+ ```
74
+
75
+ Once a filter exist, one can add to it as many Blocks as necessary
76
+
77
+ ```typescript
78
+ const blur = new BlurBlock(smartFilter, "blur");
79
+ ```
80
+
81
+ Then, the various blocks can be linked together:
82
+
83
+ ```typescript
84
+ videoInput.output.connectTo(blur.input);
85
+ blur.output.connectTo(blackAndWhite.input);
86
+ ```
87
+
88
+ Finally the last block should be linked into the Smart Filter output:
89
+
90
+ ```typescript
91
+ titleInput.output.connectTo(smartFilter.output);
92
+ ```
93
+
94
+ ### Optimizations
95
+
96
+ You can activate two optimizations once you've created your graph and have an instance of `SmartFilter` ready.
97
+
98
+ The first is a graph optimizer which attempts to "merge" the "compatible" shader blocks to create an optimized version of the graph, thus reducing the final number of draw calls (as there are as many draw calls as there are shader blocks in the graph). This optimization pass will create a new instance of `SmartFilter`, which you can use in place of the initial instance. Here's how to do it:
99
+
100
+ ```typescript
101
+ const forceMaxSamplersInFragmentShader = 0;
102
+ const vfo = new SmartFilterOptimizer(smartFilter, {
103
+ // filters is an (unoptimized) instance of SmartFilter
104
+ maxSamplersInFragmentShader: forceMaxSamplersInFragmentShader || engine.getCaps().maxTexturesImageUnits,
105
+ });
106
+ const smartFilterOptimized = vfo.optimize()!; // filters is now an optimized instance of SmartFilter
107
+ ```
108
+
109
+ One caveat is that a fragment shader has a limited number of samplers it can use, so we shouldn't merge blocks (even if they're compatible) once we've reached this limit. You should normally use the current GPU limit, so keep `forceMaxSamplersInFragmentShader` at 0 in the code above, to use the correct value for the GPU.
110
+
111
+ The second optimization is a texture analyzer that traverses the graph and recycles textures between blocks (where possible), in order to limit the total number of textures used by the graph. You can activate it as follows:
112
+
113
+ ```typescript
114
+ const rtg = new RenderTargetGenerator(true); // true to minimize the number of textures created
115
+ const runtime = await filter.createRuntimeAsync(this.engine, rtg);
116
+ ```
117
+
118
+ See the next section for details of how to create a runtime.
119
+
120
+ ### Runtime
121
+
122
+ To keep a nice separation between edition time, resource management and render time, the filter itself cannot be rendered directly. In order to do so, a runtime needs to be created from the filter:
123
+
124
+ ```typescript
125
+ const runtime = await smartFilter.createRuntimeAsync(engine);
126
+ ```
127
+
128
+ _Note_ the same filter can be used across different runtimes.
129
+
130
+ The runtime contains the list of resources required to render the filter like (intermediate textures, shaders and buffers). It is also owning a Command buffer containing the list of all the actions required to display the filter.
131
+
132
+ For instance the content of the command buffer for the simplest filter would be:
133
+
134
+ ```
135
+ ----- Command buffer commands -----
136
+ Owner: OutputBlock (output) - Command: OutputBlock.render
137
+ -----------------------------------
138
+ ```
139
+
140
+ Whereas the one of a complex one could look like:
141
+
142
+ ```
143
+ ----- Command buffer commands -----
144
+ Owner: DirectionalBlurBlock (blurIV) - Command: DirectionalBlurBlock.render
145
+ Owner: DirectionalBlurBlock (blurIH) - Command: DirectionalBlurBlock.render
146
+ Owner: DirectionalBlurBlock (blurV) - Command: DirectionalBlurBlock.render
147
+ Owner: DirectionalBlurBlock (blurH) - Command: DirectionalBlurBlock.render
148
+ Owner: BlackAndWhiteBlock (blackAndWhite) - Command: BlackAndWhiteBlock.render
149
+ Owner: FrameBlock (frame) - Command: FrameBlock.renderToCanvas
150
+ -----------------------------------
151
+ ```
152
+
153
+ The command buffer is accessible through the runtime for debugging and logging purpose. The list of command could therefore be extended with custom ones if necessary of be parsed for introspection purpose as we do in our [logger](./src/command/commandBufferDebugger.ts).
154
+
155
+ Rendering the current runtime is as simple as `runtime.render();`.
156
+
157
+ _Note_ The runtime should be disposed once it is not used anymore to free the GPU memory and prevent leaks.
158
+
159
+ ## A few core Principles
160
+
161
+ The overall system is trying at best to follow 3 simple rules:
162
+
163
+ - Be CPU efficient: for instance, we are trying to be branchless in most of our commands and we try to keep the number of commands as low as possible.
164
+ - Be memory efficient: no commands should allocate memory as it could trigger some garbage collection at the expense of frame loss.
165
+ - Be GPU efficient: the graph and texture optimizers minimize the number of "passes" required to render an image and the GPU resources used by the graph.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Define an interface for all classes that will hold resources
3
+ */
4
+ export interface IDisposable {
5
+ /**
6
+ * Releases all held resources
7
+ */
8
+ dispose(): void;
9
+ }
@@ -0,0 +1,121 @@
1
+ import type { ConnectionPoint } from "../connection/connectionPoint";
2
+ import type { ConnectionPointType } from "../connection/connectionPointType";
3
+
4
+ import { BaseBlock } from "../blocks/baseBlock.js";
5
+
6
+ /**
7
+ * The aggregate block class is the base class for all blocks that be created from other blocks.
8
+ *
9
+ * It is responsible for managing a hidden chain of smart filter blocks in order and expose them through
10
+ * its own connection points.
11
+ *
12
+ * The internal state is basically a filter itself.
13
+ */
14
+ export abstract class AggregateBlock extends BaseBlock {
15
+ /**
16
+ * The class name of the block.
17
+ */
18
+ public static override ClassName = "AggregateBlock";
19
+
20
+ /**
21
+ * The list of relationships between the internal graph output and the outside ones.
22
+ */
23
+ private readonly _aggregatedOutputs: [ConnectionPoint, ConnectionPoint][] = [];
24
+
25
+ /**
26
+ * The list of relationships between the internal graph inputs and the outside ones.
27
+ */
28
+ private readonly _aggregatedInputs: [ConnectionPoint, ConnectionPoint][] = [];
29
+
30
+ /**
31
+ * @internal
32
+ * Merges the internal graph into the SmartFilter
33
+ */
34
+ public _mergeIntoSmartFilter(mergedAggregateBlocks: AggregateBlock[]): void {
35
+ // Rewire output connections
36
+ for (const [internalConnectionPoint, externalConnectionPoint] of this._aggregatedOutputs) {
37
+ const endpointsToConnectTo = externalConnectionPoint.endpoints.slice();
38
+ externalConnectionPoint.disconnectAllEndpoints();
39
+ for (const endpoint of endpointsToConnectTo) {
40
+ internalConnectionPoint.connectTo(endpoint);
41
+ }
42
+ }
43
+
44
+ // Rewire input connections
45
+ for (const [internalConnectionPoint, externalConnectionPoint] of this._aggregatedInputs) {
46
+ const connectedToExternalConnectionPoint = externalConnectionPoint.connectedTo;
47
+ if (connectedToExternalConnectionPoint) {
48
+ connectedToExternalConnectionPoint.disconnectFrom(externalConnectionPoint);
49
+ connectedToExternalConnectionPoint.connectTo(internalConnectionPoint);
50
+ }
51
+ }
52
+
53
+ // Tell any internal aggregate blocks to merge
54
+ // Must be done after the inputs and outputs were merged at our level, or the internal aggregate block may not be wired up to anything
55
+ for (const aggregateOutput of this._aggregatedOutputs) {
56
+ const internalConnectionPoint = aggregateOutput[0];
57
+ internalConnectionPoint.ownerBlock.visit({}, (block: BaseBlock, _extraData: Object) => {
58
+ if (block instanceof AggregateBlock) {
59
+ block._mergeIntoSmartFilter(mergedAggregateBlocks);
60
+ }
61
+ });
62
+ }
63
+
64
+ // Add ourselves to the list of merged aggregate blocks
65
+ mergedAggregateBlocks.push(this);
66
+ }
67
+
68
+ /**
69
+ * @internal
70
+ * Undoes a previous mergeIntoSmartFilter call.
71
+ */
72
+ public _unmergeFromSmartFilter(): void {
73
+ for (const [internalConnectionPoint, externalConnectionPoint] of this._aggregatedOutputs) {
74
+ const endpointsToConnectTo = internalConnectionPoint.endpoints.slice();
75
+ internalConnectionPoint.disconnectAllEndpoints();
76
+ for (const endpoint of endpointsToConnectTo) {
77
+ externalConnectionPoint.connectTo(endpoint);
78
+ }
79
+ }
80
+
81
+ for (const [internalConnectionPoint, externalConnectionPoint] of this._aggregatedInputs) {
82
+ const connectedToInternalConnectionPoint = internalConnectionPoint.connectedTo;
83
+ if (connectedToInternalConnectionPoint) {
84
+ connectedToInternalConnectionPoint.disconnectFrom(internalConnectionPoint);
85
+ connectedToInternalConnectionPoint.connectTo(externalConnectionPoint);
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Registers an input connection from the internal graph as an input of the aggregated graph.
92
+ * @param name - The name of the exposed input connection point
93
+ * @param internalConnectionPoint - The input connection point in the inner graph to expose as an input on the aggregate block
94
+ * @returns the connection point referencing the input block
95
+ */
96
+ protected _registerSubfilterInput<U extends ConnectionPointType>(
97
+ name: string,
98
+ internalConnectionPoint: ConnectionPoint<U>
99
+ ): ConnectionPoint<U> {
100
+ const externalInputConnectionPoint = this._registerInput(name, internalConnectionPoint.type);
101
+
102
+ this._aggregatedInputs.push([internalConnectionPoint, externalInputConnectionPoint]);
103
+ return externalInputConnectionPoint;
104
+ }
105
+
106
+ /**
107
+ * Registers an output connection point from the internal graph as an output of the aggregated graph.
108
+ * @param name - The name of the exposed output connection point
109
+ * @param internalConnectionPoint - The output connection point in the inner graph to expose as an output on the aggregate block
110
+ * @returns the connection point referencing the output connection point
111
+ */
112
+ protected _registerSubfilterOutput<U extends ConnectionPointType>(
113
+ name: string,
114
+ internalConnectionPoint: ConnectionPoint<U>
115
+ ): ConnectionPoint<U> {
116
+ const externalOutputConnectionPoint = this._registerOutput(name, internalConnectionPoint.type);
117
+
118
+ this._aggregatedOutputs.push([internalConnectionPoint, externalOutputConnectionPoint]);
119
+ return externalOutputConnectionPoint;
120
+ }
121
+ }
@@ -0,0 +1,341 @@
1
+ import type { Nullable } from "@babylonjs/core/types";
2
+ import { UniqueIdGenerator } from "@babylonjs/core/Misc/uniqueIdGenerator.js";
3
+
4
+ import { ConnectionPointType, type ConnectionPointValue } from "../connection/connectionPointType.js";
5
+ import type { InitializationData, SmartFilter } from "../smartFilter";
6
+ import type { ICommandOwner } from "../command/command";
7
+ import { ConnectionPoint, type RuntimeData } from "../connection/connectionPoint.js";
8
+ import { ConnectionPointWithDefault } from "../connection/connectionPointWithDefault.js";
9
+ import { ConnectionPointDirection } from "../connection/connectionPointDirection.js";
10
+
11
+ /**
12
+ * Defines a callback function that is triggered when visiting a block,
13
+ * It also carries over extra data preventing the need to use global variables or closures.
14
+ */
15
+ export type BlockVisitor<T extends object> = (block: BaseBlock, extraData: T) => void;
16
+
17
+ /**
18
+ * This class represents the base class for all smart filter blocks.
19
+ *
20
+ * It defines the basic structure of a smart filter block and provides the base implementation for
21
+ * managing the connection points.
22
+ *
23
+ * It enforces common behavior for all smart filter blocks.
24
+ */
25
+ export abstract class BaseBlock implements ICommandOwner {
26
+ protected static _alreadyVisitedBlocks = new Set<BaseBlock>();
27
+
28
+ /**
29
+ * The class name of the block.
30
+ */
31
+ public static ClassName = "BaseBlock";
32
+
33
+ /**
34
+ * The smart filter the block belongs to.
35
+ */
36
+ public readonly smartFilter: SmartFilter;
37
+
38
+ /**
39
+ * Global unique id of the block (This is unique for the current session).
40
+ */
41
+ public readonly uniqueId: number;
42
+
43
+ /**
44
+ * The name of the block. This is used to identify the block in the smart filter or in debug.
45
+ */
46
+ public readonly name: string;
47
+
48
+ /**
49
+ * User provided comments about the block. It can be used to document the block.
50
+ */
51
+ public comments: Nullable<string> = null;
52
+
53
+ private readonly _inputs: ConnectionPoint[] = [];
54
+ private readonly _outputs: ConnectionPoint[] = [];
55
+
56
+ /**
57
+ * Instantiates a new block.
58
+ * @param smartFilter - Defines the smart filter the block belongs to
59
+ * @param name - Defines the name of the block
60
+ * @param disableOptimization - Defines if the block is optimizable or not
61
+ */
62
+ constructor(
63
+ smartFilter: SmartFilter,
64
+ name: string,
65
+ public readonly disableOptimization = false
66
+ ) {
67
+ this.uniqueId = UniqueIdGenerator.UniqueId;
68
+ this.name = name;
69
+ this.smartFilter = smartFilter;
70
+
71
+ // Register the block in the smart filter
72
+ smartFilter.registerBlock(this);
73
+ }
74
+
75
+ /**
76
+ * Returns the inputs connection points of the current block.
77
+ */
78
+ public get inputs(): ReadonlyArray<ConnectionPoint> {
79
+ return this._inputs;
80
+ }
81
+
82
+ /**
83
+ * Returns the outputs connection points of the current block.
84
+ */
85
+ public get outputs(): ReadonlyArray<ConnectionPoint> {
86
+ return this._outputs;
87
+ }
88
+
89
+ /**
90
+ * Returns if the block is an input block.
91
+ */
92
+ public get isInput(): boolean {
93
+ return this._inputs.length === 0;
94
+ }
95
+
96
+ /**
97
+ * Returns if the block is an output block.
98
+ */
99
+ public get isOutput(): boolean {
100
+ return this._outputs.length === 0;
101
+ }
102
+
103
+ /**
104
+ * @returns the class name of the block
105
+ */
106
+ public getClassName(): string {
107
+ // Note that we use a static property instead of doing this.constructor.name to avoid problems with minifiers that would change the name of the class
108
+ return (this.constructor as typeof BaseBlock).ClassName;
109
+ }
110
+
111
+ /**
112
+ * Checks if the block is an "ancestor" of another giving block.
113
+ * @param block - Defines the block to check against
114
+ * @returns True if the block is an ancestor of the given block, otherwise false
115
+ */
116
+ public isAnAncestorOf(block: BaseBlock): boolean {
117
+ for (const output of this._outputs) {
118
+ if (!output.endpoints.length) {
119
+ continue;
120
+ }
121
+
122
+ for (const endpoint of output.endpoints) {
123
+ if (endpoint.ownerBlock === block) {
124
+ return true;
125
+ }
126
+ if (endpoint.ownerBlock.isAnAncestorOf(block)) {
127
+ return true;
128
+ }
129
+ }
130
+ }
131
+
132
+ return false;
133
+ }
134
+
135
+ protected _visitInputs<T extends object>(
136
+ extraData: T,
137
+ callback: BlockVisitor<T>,
138
+ alreadyVisited: Set<BaseBlock>
139
+ ): void {
140
+ for (const input of this.inputs) {
141
+ if (!input.connectedTo) {
142
+ continue;
143
+ }
144
+
145
+ const block = input.connectedTo.ownerBlock;
146
+
147
+ block.visit(extraData, callback, alreadyVisited);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Visits the block and its inputs recursively.
153
+ * When starting from the smart filter output block, this will visit all the blocks in the smart filter.
154
+ * Note that it's a depth first visit: the callback is called on the block AFTER visiting its inputs.
155
+ * @param extraData - The extra data to pass to the callback
156
+ * @param callback - The callback to call on each block
157
+ * @param alreadyVisitedBlocks - Defines the set of blocks already visited (if not provided, a new set will be created)
158
+ */
159
+ public visit<T extends object>(
160
+ extraData: T,
161
+ callback: BlockVisitor<T>,
162
+ alreadyVisitedBlocks?: Set<BaseBlock>
163
+ ): void {
164
+ if (!alreadyVisitedBlocks) {
165
+ alreadyVisitedBlocks = BaseBlock._alreadyVisitedBlocks;
166
+ alreadyVisitedBlocks.clear();
167
+ }
168
+
169
+ if (!alreadyVisitedBlocks.has(this)) {
170
+ alreadyVisitedBlocks.add(this);
171
+
172
+ this._visitInputs(extraData, callback, alreadyVisitedBlocks);
173
+
174
+ callback(this, extraData);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Finds the input connection point with the given name.
180
+ * @param name - Name of the input to find
181
+ * @returns The connection point with the given name or null if not found
182
+ */
183
+ public findInput<U extends ConnectionPointType>(name: string): Nullable<ConnectionPoint<U>> {
184
+ for (const input of this._inputs) {
185
+ if (input.name === name) {
186
+ return input as ConnectionPoint<U>;
187
+ }
188
+ }
189
+
190
+ return null;
191
+ }
192
+
193
+ /**
194
+ * Disconnects the block from the graph.
195
+ * @param _disconnectedConnections - Stores the connections that have been broken in the process. You can reconnect them later if needed.
196
+ */
197
+ public disconnectFromGraph(_disconnectedConnections?: [ConnectionPoint, ConnectionPoint][]): void {}
198
+
199
+ /**
200
+ * Prepares the block for runtime.
201
+ * This is called by the smart filter just before creating the smart filter runtime, and by the optimizer.
202
+ */
203
+ public prepareForRuntime(): void {}
204
+
205
+ /**
206
+ * Propagates the runtime data - telling all outputs to propagate their runtime data forward through the graph
207
+ */
208
+ public propagateRuntimeData(): void {
209
+ for (const output of this._outputs) {
210
+ output.propagateRuntimeData();
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Generates the commands needed to execute the block at runtime and gathers promises for initialization work
216
+ * @param initializationData - The initialization data to use
217
+ * @param _finalOutput - Defines if the block is the final output of the smart filter
218
+ */
219
+ public generateCommandsAndGatherInitPromises(initializationData: InitializationData, _finalOutput: boolean): void {
220
+ // Check if any inputs are Textures which aren't yet ready, and if so, ensure init waits for them to be ready
221
+ for (const input of this._inputs) {
222
+ if (input.type === ConnectionPointType.Texture) {
223
+ const texture = input.runtimeData?.value as Nullable<ConnectionPointValue<typeof input.type>>;
224
+ if (texture && !texture.isReady()) {
225
+ const internalTexture = texture.getInternalTexture();
226
+ if (internalTexture) {
227
+ const textureReadyPromise = new Promise<void>((resolve, reject) => {
228
+ internalTexture.onLoadedObservable.add(() => {
229
+ resolve();
230
+ });
231
+ internalTexture.onErrorObservable.add((error) => {
232
+ reject(error);
233
+ });
234
+ });
235
+ initializationData.initializationPromises.push(textureReadyPromise);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Disconnects all the inputs and outputs from the Block.
244
+ */
245
+ public disconnect(): void {
246
+ // Detach inputs
247
+ for (const input of this._inputs) {
248
+ input.connectedTo?.disconnectFrom(input);
249
+ }
250
+
251
+ // Detach outputs
252
+ for (const output of this._outputs) {
253
+ output.disconnectAllEndpoints();
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Registers a new input connection point in the block which must have a connection before the graph can be used.
259
+ * @param name - Defines the name of the input connection point
260
+ * @param type - Defines the type of the input connection point
261
+ * @param defaultValue - Defines the optional default value of the input connection point to use if not connection is made
262
+ * @returns The new ConnectionPoint
263
+ * @internal
264
+ */
265
+ public _registerInput<U extends ConnectionPointType>(
266
+ name: string,
267
+ type: U,
268
+ defaultValue: Nullable<RuntimeData<U>> = null
269
+ ): ConnectionPoint<U> {
270
+ const input = new ConnectionPoint(name, this, type, ConnectionPointDirection.Input, defaultValue);
271
+ this._inputs.push(input);
272
+ return input;
273
+ }
274
+
275
+ /**
276
+ * Registers a new input connection point in the block which doesn't require a connection because it has a default value.
277
+ * @param name - Defines the name of the input connection point
278
+ * @param type - Defines the type of the input connection point
279
+ * @param defaultValue - Defines the default value to use if nothing is connected to this connection point
280
+ * @returns The new ConnectionPointWithDefault
281
+ * @internal
282
+ */
283
+ public _registerOptionalInput<U extends ConnectionPointType>(
284
+ name: string,
285
+ type: U,
286
+ defaultValue: RuntimeData<U>
287
+ ): ConnectionPointWithDefault<U> {
288
+ const input = new ConnectionPointWithDefault(name, this, type, ConnectionPointDirection.Input, defaultValue);
289
+ this._inputs.push(input);
290
+ return input;
291
+ }
292
+
293
+ /**
294
+ * Registers a new output connection point in the block.
295
+ * @param name - Defines the name of the output connection point
296
+ * @param type - Defines the type of the output connection point
297
+ * @returns The new output connection point
298
+ * @internal
299
+ */
300
+ public _registerOutput<U extends ConnectionPointType>(name: string, type: U): ConnectionPoint<U> {
301
+ const output = new ConnectionPoint(name, this, type, ConnectionPointDirection.Output);
302
+ this._outputs.push(output);
303
+ return output;
304
+ }
305
+
306
+ /**
307
+ * Registers a new output connection point in the block that always has runtimeData because it has a default value and doesn't allow it to be overwritten with null.
308
+ * @param name - Defines the name of the output connection point
309
+ * @param type - Defines the type of the output connection point
310
+ * @param initialValue - Defines the initial value of the output connection point
311
+ * @returns The new output connection point with a default value
312
+ * @internal
313
+ */
314
+ public _registerOutputWithDefault<U extends ConnectionPointType>(
315
+ name: string,
316
+ type: U,
317
+ initialValue: RuntimeData<U>
318
+ ): ConnectionPointWithDefault<U> {
319
+ const output = new ConnectionPointWithDefault(name, this, type, ConnectionPointDirection.Output, initialValue);
320
+ this._outputs.push(output);
321
+ return output;
322
+ }
323
+
324
+ /**
325
+ * Gets the required RuntimeData for the given input, throwing with a clear message if it is null
326
+ * @param input - The input to get the runtime data for
327
+ * @returns The runtimeData or throws if it was undefined
328
+ */
329
+ protected _confirmRuntimeDataSupplied<U extends ConnectionPointType = ConnectionPointType>(
330
+ input: ConnectionPoint<U>
331
+ ): RuntimeData<U> {
332
+ if (!input.runtimeData) {
333
+ throw new Error(
334
+ `The ${ConnectionPointType[input.type]} input named "${
335
+ input.name
336
+ }" is missing for the ${this.getClassName()} named "${this.name}"`
337
+ );
338
+ }
339
+ return input.runtimeData;
340
+ }
341
+ }