From 59dc6572bc5543e26d40982306d3d4ae3be1d9c8 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:12:14 +0100 Subject: [PATCH 01/17] Add doc --- packages/dev/core/src/FrameGraph/Passes/pass.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/FrameGraph/Passes/pass.ts b/packages/dev/core/src/FrameGraph/Passes/pass.ts index b2cd338374d..c7bd8a32076 100644 --- a/packages/dev/core/src/FrameGraph/Passes/pass.ts +++ b/packages/dev/core/src/FrameGraph/Passes/pass.ts @@ -1,29 +1,39 @@ import type { Nullable, FrameGraphContext, IFrameGraphPass, FrameGraphTask } from "core/index"; /** - * @internal + * Base class for a frame graph pass. */ export class FrameGraphPass implements IFrameGraphPass { private _executeFunc: (context: T) => void; + /** + * Whether the pass is disabled. Disabled passes will be skipped during execution. + */ public disabled = false; + /** @internal */ constructor( public name: string, protected readonly _parentTask: FrameGraphTask, protected readonly _context: T ) {} + /** + * Executes the pass. + * @param func The function to execute for the pass. + */ public setExecuteFunc(func: (context: T) => void) { this._executeFunc = func; } + /** @internal */ public _execute() { if (!this.disabled) { this._executeFunc(this._context); } } + /** @internal */ public _isValid(): Nullable { return this._executeFunc !== undefined ? null : "Execute function is not set (call setExecuteFunc to set it)"; } From 1bfc63928f00d7b7b6caa5630fa15365fd002da7 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:14:02 +0100 Subject: [PATCH 02/17] Add new computer shader task --- .../Tasks/Misc/computeShaderTask.ts | 212 ++++++++++++++++++ packages/dev/core/src/FrameGraph/index.ts | 1 + 2 files changed, 213 insertions(+) create mode 100644 packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts diff --git a/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts b/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts new file mode 100644 index 00000000000..ee5e56a6b69 --- /dev/null +++ b/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts @@ -0,0 +1,212 @@ +import type { + BaseTexture, + DataBuffer, + ExternalTexture, + FrameGraph, + FrameGraphContext, + FrameGraphPass, + IComputeShaderOptions, + IComputeShaderPath, + InternalTexture, + StorageBuffer, + TextureSampler, + VideoTexture, +} from "core/index"; +import { FrameGraphTask } from "../../frameGraphTask"; +import { ComputeShader } from "core/Compute/computeShader"; +import { Vector3 } from "core/Maths/math.vector"; +import { UniformBuffer } from "core/Materials/uniformBuffer"; + +/** + * Task used to execute a compute shader (WebGPU only) + */ +export class FrameGraphComputeShaderTask extends FrameGraphTask { + private readonly _cs: ComputeShader; + private readonly _ubo: { [name: string]: { ubo: UniformBuffer; autoUpdate: boolean } }; + + /** + * Defines the dispatch size for the compute shader + */ + public dispatchSize = new Vector3(1, 1, 1); + + /** + * Defines an indirect dispatch buffer and offset. + * If set, this will be used instead of the dispatchSize property and an indirect dispatch will be performed. + * "offset" is the offset in the buffer where the workgroup counts are stored (default: 0) + */ + public indirectDispatch?: { buffer: StorageBuffer | DataBuffer; offset?: number }; + + /** + * An optional execute function that will be called at the beginning of the task execution + */ + public execute?: (context: FrameGraphContext) => void; + + /** + * Gets the compute shader used by the task + */ + public get computeShader(): ComputeShader { + return this._cs; + } + + /** + * Gets a uniform buffer created by a call to createUniformBuffer() + * @param name Name of the uniform buffer + * @returns The uniform buffer + */ + public getUniformBuffer(name: string): UniformBuffer { + return this._ubo[name]?.ubo; + } + + /** + * Creates a new compute shader task. + * @param name The name of the task. + * @param frameGraph The frame graph the task belongs to. + * @param shaderPath Defines the route to the shader code in one of three ways: + * * object: \{ compute: "custom" \}, used with ShaderStore.ShadersStoreWGSL["customComputeShader"] + * * object: \{ computeElement: "HTMLElementId" \}, used with shader code in script tags + * * object: \{ computeSource: "compute shader code string" \}, where the string contains the shader code + * * string: try first to find the code in ShaderStore.ShadersStoreWGSL[shaderPath + "ComputeShader"]. If not, assumes it is a file with name shaderPath.compute.fx in index.html folder. + * @param options Define the options used to create the shader + */ + constructor(name: string, frameGraph: FrameGraph, shaderPath: IComputeShaderPath | string, options: Partial = {}) { + super(name, frameGraph); + + this._cs = new ComputeShader(name + "_cs", frameGraph.engine, shaderPath, options); + this._ubo = {}; + } + + public override isReady(): boolean { + return this._cs.isReady(); + } + + /** + * Creates a uniform buffer and binds it to the shader + * @param name Name of the uniform buffer + * @param description Description of the uniform buffer: names and sizes (in floats) of the uniforms + * @param autoUpdate If the UBO must be updated automatically before each dispatch (default: true) + * @returns The created uniform buffer + */ + public createUniformBuffer(name: string, description: { [name: string]: number }, autoUpdate = true): UniformBuffer { + const uBuffer = new UniformBuffer(this._frameGraph.engine); + + this._ubo[name] = { ubo: uBuffer, autoUpdate }; + + for (const key in description) { + uBuffer.addUniform(key, description[key]); + } + + this._cs.setUniformBuffer(name, uBuffer); + + return uBuffer; + } + + /** + * Binds a texture to the shader + * @param name Binding name of the texture + * @param texture Texture to bind + * @param bindSampler Bind the sampler corresponding to the texture (default: true). The sampler will be bound just before the binding index of the texture + */ + public setTexture(name: string, texture: BaseTexture, bindSampler = true): void { + this._cs.setTexture(name, texture, bindSampler); + } + + /** + * Binds an internal texture to the shader + * @param name Binding name of the texture + * @param texture Texture to bind + */ + public setInternalTexture(name: string, texture: InternalTexture): void { + this._cs.setInternalTexture(name, texture); + } + + /** + * Binds a storage texture to the shader + * @param name Binding name of the texture + * @param texture Texture to bind + */ + public setStorageTexture(name: string, texture: BaseTexture): void { + this._cs.setStorageTexture(name, texture); + } + + /** + * Binds an external texture to the shader + * @param name Binding name of the texture + * @param texture Texture to bind + */ + public setExternalTexture(name: string, texture: ExternalTexture): void { + this._cs.setExternalTexture(name, texture); + } + + /** + * Binds a video texture to the shader (by binding the external texture attached to this video) + * @param name Binding name of the texture + * @param texture Texture to bind + * @returns true if the video texture was successfully bound, else false. false will be returned if the current engine does not support external textures + */ + public setVideoTexture(name: string, texture: VideoTexture) { + return this._cs.setVideoTexture(name, texture); + } + + /** + * Binds a uniform buffer to the shader + * @param name Binding name of the buffer + * @param buffer Buffer to bind + */ + public setUniformBuffer(name: string, buffer: UniformBuffer | DataBuffer): void { + this._cs.setUniformBuffer(name, buffer); + } + + /** + * Binds a storage buffer to the shader + * @param name Binding name of the buffer + * @param buffer Buffer to bind + */ + public setStorageBuffer(name: string, buffer: StorageBuffer | DataBuffer): void { + this._cs.setStorageBuffer(name, buffer); + } + + /** + * Binds a texture sampler to the shader + * @param name Binding name of the sampler + * @param sampler Sampler to bind + */ + public setTextureSampler(name: string, sampler: TextureSampler): void { + this._cs.setTextureSampler(name, sampler); + } + + public record(skipCreationOfDisabledPasses?: boolean): FrameGraphPass { + const pass = this._frameGraph.addPass(this.name); + + pass.setExecuteFunc((context) => { + this.execute?.(context); + + for (const key in this._ubo) { + const uboEntry = this._ubo[key]; + if (uboEntry.autoUpdate) { + uboEntry.ubo.update(); + } + } + + if (this.indirectDispatch) { + this._cs.dispatchIndirect(this.indirectDispatch.buffer, this.indirectDispatch.offset); + } else { + this._cs.dispatch(this.dispatchSize.x, this.dispatchSize.y, this.dispatchSize.z); + } + }); + + if (!skipCreationOfDisabledPasses) { + const passDisabled = this._frameGraph.addPass(this.name + "_disabled", true); + + passDisabled.setExecuteFunc(() => {}); + } + + return pass; + } + + public override dispose(): void { + for (const key in this._ubo) { + this._ubo[key].ubo.dispose(); + } + super.dispose(); + } +} diff --git a/packages/dev/core/src/FrameGraph/index.ts b/packages/dev/core/src/FrameGraph/index.ts index d593f9cf7a1..8aefdb085b9 100644 --- a/packages/dev/core/src/FrameGraph/index.ts +++ b/packages/dev/core/src/FrameGraph/index.ts @@ -13,6 +13,7 @@ export * from "./Passes/renderPass"; export * from "./Tasks/Layers/glowLayerTask"; export * from "./Tasks/Layers/highlightLayerTask"; +export * from "./Tasks/Misc/computeShaderTask"; export * from "./Tasks/Misc/cullObjectsTask"; export * from "./Tasks/Misc/executeTask"; From e7996f1fdf7a3f3ee93c23cadbdcd5a7b413df02 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:14:17 +0100 Subject: [PATCH 03/17] Add customIsReady property --- .../dev/core/src/FrameGraph/Tasks/Misc/executeTask.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/dev/core/src/FrameGraph/Tasks/Misc/executeTask.ts b/packages/dev/core/src/FrameGraph/Tasks/Misc/executeTask.ts index d9df3f12447..eaa083cbcc7 100644 --- a/packages/dev/core/src/FrameGraph/Tasks/Misc/executeTask.ts +++ b/packages/dev/core/src/FrameGraph/Tasks/Misc/executeTask.ts @@ -15,6 +15,15 @@ export class FrameGraphExecuteTask extends FrameGraphTask { */ public funcDisabled?: (context: FrameGraphContext) => void; + /** + * Custom readiness check (optional). + */ + public customIsReady?: () => boolean; + + public override isReady(): boolean { + return !this.customIsReady || this.customIsReady(); + } + /** * Creates a new execute task. * @param name The name of the task. From 672df1d623058394d1f48bb8cef7f1d2d644b501 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:14:33 +0100 Subject: [PATCH 04/17] Simplify code --- packages/dev/core/src/FrameGraph/frameGraph.ts | 6 +----- packages/dev/core/src/FrameGraph/frameGraphTask.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/dev/core/src/FrameGraph/frameGraph.ts b/packages/dev/core/src/FrameGraph/frameGraph.ts index 55e15bb6501..39a8dfc7057 100644 --- a/packages/dev/core/src/FrameGraph/frameGraph.ts +++ b/packages/dev/core/src/FrameGraph/frameGraph.ts @@ -306,11 +306,7 @@ export class FrameGraph implements IDisposable { this.textureManager._updateHistoryTextures(); for (const task of this._tasks) { - const passes = task._getPasses(); - - for (const pass of passes) { - pass._execute(); - } + task._execute(); } this._renderContext.bindRenderTarget(undefined, undefined, true); // restore default framebuffer diff --git a/packages/dev/core/src/FrameGraph/frameGraphTask.ts b/packages/dev/core/src/FrameGraph/frameGraphTask.ts index 2040196aca3..ee9fc392068 100644 --- a/packages/dev/core/src/FrameGraph/frameGraphTask.ts +++ b/packages/dev/core/src/FrameGraph/frameGraphTask.ts @@ -62,8 +62,9 @@ export abstract class FrameGraphTask { /** * Records the task in the frame graph. Use this function to add content (render passes, ...) to the task. + * @param skipCreationOfDisabledPasses If true, the disabled passe(s) won't be created. */ - public abstract record(): void; + public abstract record(skipCreationOfDisabledPasses?: boolean): void; /** * An observable that is triggered after the textures have been allocated. @@ -185,8 +186,12 @@ export abstract class FrameGraphTask { } /** @internal */ - public _getPasses(): IFrameGraphPass[] { - return this.disabled && this._passesDisabled.length > 0 ? this._passesDisabled : this._passes; + public _execute() { + const passes = this._disabled && this._passesDisabled.length > 0 ? this._passesDisabled : this._passes; + + for (const pass of passes) { + pass._execute(); + } } private _checkSameRenderTarget(src: Nullable[]>, dst: Nullable[]>) { From baf1c253c6fd828b385fcc304c9e5ed2016abf67 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:15:23 +0100 Subject: [PATCH 05/17] Fix directional and point lights not working with frame graphs --- packages/dev/core/src/Lights/directionalLight.ts | 8 ++------ packages/dev/core/src/Lights/pointLight.ts | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/dev/core/src/Lights/directionalLight.ts b/packages/dev/core/src/Lights/directionalLight.ts index a83be0392a7..93930218c56 100644 --- a/packages/dev/core/src/Lights/directionalLight.ts +++ b/packages/dev/core/src/Lights/directionalLight.ts @@ -181,15 +181,11 @@ export class DirectionalLight extends ShadowLight { protected _setDefaultFixedFrustumShadowProjectionMatrix(matrix: Matrix): void { const activeCamera = this.getScene().activeCamera; - if (!activeCamera) { - return; - } - Matrix.OrthoLHToRef( this.shadowFrustumSize, this.shadowFrustumSize, - this.shadowMinZ !== undefined ? this.shadowMinZ : activeCamera.minZ, - this.shadowMaxZ !== undefined ? this.shadowMaxZ : activeCamera.maxZ, + this.shadowMinZ !== undefined ? this.shadowMinZ : activeCamera ? activeCamera.minZ : Constants.ShadowMinZ, + this.shadowMaxZ !== undefined ? this.shadowMaxZ : activeCamera ? activeCamera.maxZ : Constants.ShadowMaxZ, matrix, this.getScene().getEngine().isNDCHalfZRange ); diff --git a/packages/dev/core/src/Lights/pointLight.ts b/packages/dev/core/src/Lights/pointLight.ts index 73f2ae7f4ff..775d16fb6b9 100644 --- a/packages/dev/core/src/Lights/pointLight.ts +++ b/packages/dev/core/src/Lights/pointLight.ts @@ -149,12 +149,8 @@ export class PointLight extends ShadowLight { protected _setDefaultShadowProjectionMatrix(matrix: Matrix, viewMatrix: Matrix, renderList: Array): void { const activeCamera = this.getScene().activeCamera; - if (!activeCamera) { - return; - } - - const minZ = this.shadowMinZ !== undefined ? this.shadowMinZ : activeCamera.minZ; - const maxZ = this.shadowMaxZ !== undefined ? this.shadowMaxZ : activeCamera.maxZ; + const minZ = this.getDepthMinZ(activeCamera); + const maxZ = this.getDepthMaxZ(activeCamera); const useReverseDepthBuffer = this.getScene().getEngine().useReverseDepthBuffer; From 6d44d0524d29071fd5d071754fb9aa9276362bfd Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:15:46 +0100 Subject: [PATCH 06/17] Fix ignoreCameraMaxZ not working with frame graphs --- packages/dev/core/src/Meshes/mesh.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/core/src/Meshes/mesh.ts b/packages/dev/core/src/Meshes/mesh.ts index 065c6eb0444..e94041054f2 100644 --- a/packages/dev/core/src/Meshes/mesh.ts +++ b/packages/dev/core/src/Meshes/mesh.ts @@ -2590,7 +2590,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { const engine = scene.getEngine(); let oldCameraMaxZ = 0; let oldCamera: Nullable = null; - if (this.ignoreCameraMaxZ && scene.activeCamera && !scene._isInIntermediateRendering()) { + if (this.ignoreCameraMaxZ && scene.activeCamera) { oldCameraMaxZ = scene.activeCamera.maxZ; oldCamera = scene.activeCamera; scene.activeCamera.maxZ = 0; From 32311f0916290d98cfc10c753cef02c7aade8af2 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:16:17 +0100 Subject: [PATCH 07/17] Fix GC in floating origin mode --- packages/dev/core/src/scene.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/scene.ts b/packages/dev/core/src/scene.ts index d17e9f6f5cb..96baa45d508 100644 --- a/packages/dev/core/src/scene.ts +++ b/packages/dev/core/src/scene.ts @@ -242,6 +242,7 @@ export class Scene implements IAnimatable, IClipPlanesHolder, IAssetContainer { private _clearColor: Color4 = new Color4(0.2, 0.2, 0.3, 1.0); + private _tempVect3 = new Vector3(); private _tempVect4 = new Vector4(); /** * Observable triggered when the performance priority is changed @@ -2796,7 +2797,13 @@ export class Scene implements IAnimatable, IClipPlanesHolder, IAssetContainer { * When floatingOriginMode is enabled, offset is equal to the active camera position in world space. If no active camera or floatingOriginMode is disabled, offset is 0. */ public get floatingOriginOffset(): Vector3 { - return this.floatingOriginMode && this.activeCamera ? this.activeCamera.getWorldMatrix().getTranslation() : this._floatingOriginOffsetDefault; + return this.floatingOriginMode + ? this._mirroredCameraPosition + ? this._mirroredCameraPosition + : this.activeCamera + ? this.activeCamera.getWorldMatrix().getTranslationToRef(this._tempVect3) + : this._floatingOriginOffsetDefault + : this._floatingOriginOffsetDefault; } /** From f44d23b701b8316d4b8e02e8c790dda8f0a3ce30 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:17:14 +0100 Subject: [PATCH 08/17] Fix crash in NRGE --- .../src/components/preview/previewManager.ts | 2 ++ .../tools/nodeRenderGraphEditor/src/globalState.ts | 1 + .../tools/nodeRenderGraphEditor/src/graphEditor.tsx | 10 ++++++++++ .../nodeRenderGraphEditor/src/nodeRenderGraphEditor.ts | 1 + 4 files changed, 14 insertions(+) diff --git a/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts b/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts index a67bc6926d8..5456a5a0c23 100644 --- a/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts +++ b/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts @@ -431,7 +431,9 @@ export class PreviewManager { try { this._nodeRenderGraph.build(); + this._nodeRenderGraph.frameGraph.pausedExecution = true; await this._nodeRenderGraph.whenReadyAsync(16, 5000); + this._nodeRenderGraph.frameGraph.pausedExecution = false; this._scene.frameGraph = this._nodeRenderGraph.frameGraph; } catch (err) { if (LogErrorTrace) { diff --git a/packages/tools/nodeRenderGraphEditor/src/globalState.ts b/packages/tools/nodeRenderGraphEditor/src/globalState.ts index 3d7ca304ccc..a065c23acb0 100644 --- a/packages/tools/nodeRenderGraphEditor/src/globalState.ts +++ b/packages/tools/nodeRenderGraphEditor/src/globalState.ts @@ -23,6 +23,7 @@ export class GlobalState { hostElement: HTMLElement; hostDocument: Document; hostWindow: Window; + hostScene?: Scene; stateManager: StateManager; onClearUndoStack = new Observable(); onBuiltObservable = new Observable(); diff --git a/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx b/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx index 2dda0b66da7..b9a362d1b41 100644 --- a/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx +++ b/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx @@ -362,6 +362,16 @@ export class GraphEditor extends React.Component { + nodeRenderGraph.frameGraph.pausedExecution = false; + }); + } } catch (err) { if (LogErrorTrace) { (console as any).log(err); diff --git a/packages/tools/nodeRenderGraphEditor/src/nodeRenderGraphEditor.ts b/packages/tools/nodeRenderGraphEditor/src/nodeRenderGraphEditor.ts index 1b59fd0b81d..93ea07e5aae 100644 --- a/packages/tools/nodeRenderGraphEditor/src/nodeRenderGraphEditor.ts +++ b/packages/tools/nodeRenderGraphEditor/src/nodeRenderGraphEditor.ts @@ -63,6 +63,7 @@ export class NodeRenderGraphEditor { globalState.nodeRenderGraph = options.nodeRenderGraph; globalState.hostElement = hostElement; globalState.hostDocument = hostElement.ownerDocument!; + globalState.hostScene = options.hostScene; globalState.customSave = options.customSave; globalState.hostWindow = hostElement.ownerDocument.defaultView!; globalState.stateManager.hostDocument = globalState.hostDocument; From d8f624779c207152d60d67d6f171efa4e7ff1fd4 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 14:18:28 +0100 Subject: [PATCH 09/17] Reduce the default timeout in whenReadyAsync --- packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts | 4 ++-- packages/dev/core/src/FrameGraph/frameGraph.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts b/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts index f4c3c13b38a..d374d0aa2fe 100644 --- a/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts +++ b/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts @@ -366,11 +366,11 @@ export class NodeRenderGraph { * Returns a promise that resolves when the node render graph is ready to be executed * This method must be called after the graph has been built (NodeRenderGraph.build called)! * @param timeStep Time step in ms between retries (default is 16) - * @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 30000) + * @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 5000) * @returns The promise that resolves when the graph is ready */ // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public whenReadyAsync(timeStep = 16, maxTimeout = 30000): Promise { + public whenReadyAsync(timeStep = 16, maxTimeout = 5000): Promise { return this._frameGraph.whenReadyAsync(timeStep, maxTimeout); } diff --git a/packages/dev/core/src/FrameGraph/frameGraph.ts b/packages/dev/core/src/FrameGraph/frameGraph.ts index 39a8dfc7057..e435b931074 100644 --- a/packages/dev/core/src/FrameGraph/frameGraph.ts +++ b/packages/dev/core/src/FrameGraph/frameGraph.ts @@ -246,10 +246,10 @@ export class FrameGraph implements IDisposable { * Returns a promise that resolves when the frame graph is ready to be executed * This method must be called after the graph has been built (FrameGraph.build called)! * @param timeStep Time step in ms between retries (default is 16) - * @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 30000) + * @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 5000) * @returns The promise that resolves when the graph is ready */ - public async whenReadyAsync(timeStep = 16, maxTimeout = 30000): Promise { + public async whenReadyAsync(timeStep = 16, maxTimeout = 5000): Promise { let firstNotReadyTask: FrameGraphTask | null = null; return await new Promise((resolve) => { this._whenReadyAsyncCancel = _RetryWithInterval( From 96c853ddfba3d14d876e3725a6030776364cb080 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 15:02:56 +0100 Subject: [PATCH 10/17] Better fix for crash in NRGE --- .../src/FrameGraph/Node/nodeRenderGraph.ts | 6 +++++- .../src/components/preview/previewManager.ts | 4 +--- .../nodeRenderGraphEditor/src/graphEditor.tsx | 19 +++++++------------ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts b/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts index d374d0aa2fe..f23f583ac6a 100644 --- a/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts +++ b/packages/dev/core/src/FrameGraph/Node/nodeRenderGraph.ts @@ -371,7 +371,11 @@ export class NodeRenderGraph { */ // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax public whenReadyAsync(timeStep = 16, maxTimeout = 5000): Promise { - return this._frameGraph.whenReadyAsync(timeStep, maxTimeout); + this._frameGraph.pausedExecution = true; + // eslint-disable-next-line github/no-then + return this._frameGraph.whenReadyAsync(timeStep, maxTimeout).then(() => { + this._frameGraph.pausedExecution = false; + }); } /** diff --git a/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts b/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts index 5456a5a0c23..54455d8d510 100644 --- a/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts +++ b/packages/tools/nodeRenderGraphEditor/src/components/preview/previewManager.ts @@ -431,9 +431,7 @@ export class PreviewManager { try { this._nodeRenderGraph.build(); - this._nodeRenderGraph.frameGraph.pausedExecution = true; - await this._nodeRenderGraph.whenReadyAsync(16, 5000); - this._nodeRenderGraph.frameGraph.pausedExecution = false; + await this._nodeRenderGraph.whenReadyAsync(); this._scene.frameGraph = this._nodeRenderGraph.frameGraph; } catch (err) { if (LogErrorTrace) { diff --git a/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx b/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx index b9a362d1b41..adc7b558d9e 100644 --- a/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx +++ b/packages/tools/nodeRenderGraphEditor/src/graphEditor.tsx @@ -222,22 +222,22 @@ export class GraphEditor extends React.Component { + this.props.globalState.stateManager.onRebuildRequiredObservable.add(async () => { if (this.props.globalState.nodeRenderGraph) { - this.buildRenderGraph(); + await this.buildRenderGraphAsync(); } }); - this.props.globalState.onResetRequiredObservable.add((isDefault) => { + this.props.globalState.onResetRequiredObservable.add(async (isDefault) => { if (isDefault) { if (this.props.globalState.nodeRenderGraph) { - this.buildRenderGraph(); + await this.buildRenderGraphAsync(); } this.build(true); } else { this.build(); if (this.props.globalState.nodeRenderGraph) { - this.buildRenderGraph(); + await this.buildRenderGraphAsync(); } } }); @@ -340,7 +340,7 @@ export class GraphEditor extends React.Component { - nodeRenderGraph.frameGraph.pausedExecution = false; - }); + await nodeRenderGraph.whenReadyAsync(); } } catch (err) { if (LogErrorTrace) { From 2594450d17e0062b491c685ce4995b4457812ac2 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 15:03:12 +0100 Subject: [PATCH 11/17] Update template for simple frame graph --- packages/tools/playground/public/templates.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/playground/public/templates.json b/packages/tools/playground/public/templates.json index c24ffe7242c..6338ba1374a 100644 --- a/packages/tools/playground/public/templates.json +++ b/packages/tools/playground/public/templates.json @@ -40,7 +40,7 @@ "label": "Frame Graph: simple frame graph", "key": "Simple ", "documentation": "https://doc.babylonjs.com/features/featuresDeepDive/frameGraph/frameGraphBasicConcepts/frameGraphReplaceRenderLoop/", - "insertText": "const frameGraph = new BABYLON.FrameGraph(scene, true);\n\nengine.onResizeObservable.add(() => {\n frameGraph.build();\n});\n\nscene.cameraToUseForPointers = camera;\nscene.frameGraph = frameGraph;\n\nconst samples = 4;\n\nconst colorTexture = frameGraph.textureManager.createRenderTargetTexture(\"color\", {\n size: { width: 100, height: 100 },\n options: {\n createMipMaps: false,\n types: [BABYLON.Constants.TEXTURETYPE_UNSIGNED_BYTE],\n formats: [BABYLON.Constants.TEXTUREFORMAT_RGBA],\n samples,\n useSRGBBuffers: [false],\n labels: [\"color\"],\n },\n sizeIsPercentage: true,\n});\n\nconst depthTexture = frameGraph.textureManager.createRenderTargetTexture(\"depth\", {\n size: { width: 100, height: 100 },\n options: {\n createMipMaps: false,\n types: [BABYLON.Constants.TEXTURETYPE_UNSIGNED_BYTE],\n formats: [BABYLON.Constants.TEXTUREFORMAT_DEPTH32_FLOAT],\n samples,\n useSRGBBuffers: [false],\n labels: [\"depth\"],\n },\n sizeIsPercentage: true,\n});\n\nconst clearTask = new BABYLON.FrameGraphClearTextureTask(\"clear\", frameGraph);\n\nclearTask.clearColor = true;\nclearTask.clearDepth = true;\nclearTask.targetTexture = colorTexture;\nclearTask.depthTexture = depthTexture;\n\nframeGraph.addTask(clearTask);\n\nconst rlist = {\n meshes: scene.meshes,\n particleSystems: scene.particleSystems,\n}\n\nconst renderTask = new BABYLON.FrameGraphObjectRendererTask(\"renderObjects\", frameGraph, scene);\n\nrenderTask.targetTexture = clearTask.outputTexture;\nrenderTask.depthTexture = clearTask.outputDepthTexture;\nrenderTask.objectList = rlist;\nrenderTask.camera = camera;\n\nframeGraph.addTask(renderTask);\n\nconst copyToBackbufferTask = new BABYLON.FrameGraphCopyToBackbufferColorTask(\"copytobackbuffer\", frameGraph);\n\ncopyToBackbufferTask.sourceTexture = renderTask.outputTexture;\n\nframeGraph.addTask(copyToBackbufferTask);\n\nframeGraph.build();\n\nawait frameGraph.whenReadyAsync();" + "insertText": "const frameGraph = new BABYLON.FrameGraph(scene, true);\n\nengine.onResizeObservable.add(() => {\n frameGraph.build();\n});\n\nscene.cameraToUseForPointers = camera;\nscene.frameGraph = frameGraph;\n\nconst samples = 4;\n\nconst colorTexture = frameGraph.textureManager.createRenderTargetTexture(\"color\", {\n size: { width: 100, height: 100 },\n options: {\n createMipMaps: false,\n types: [BABYLON.Constants.TEXTURETYPE_UNSIGNED_BYTE],\n formats: [BABYLON.Constants.TEXTUREFORMAT_RGBA],\n samples,\n useSRGBBuffers: [false],\n labels: [\"color\"],\n },\n sizeIsPercentage: true,\n});\n\nconst depthTexture = frameGraph.textureManager.createRenderTargetTexture(\"depth\", {\n size: { width: 100, height: 100 },\n options: {\n createMipMaps: false,\n types: [BABYLON.Constants.TEXTURETYPE_UNSIGNED_BYTE],\n formats: [BABYLON.Constants.TEXTUREFORMAT_DEPTH32_FLOAT],\n samples,\n useSRGBBuffers: [false],\n labels: [\"depth\"],\n },\n sizeIsPercentage: true,\n});\n\nconst clearTask = new BABYLON.FrameGraphClearTextureTask(\"clear\", frameGraph);\n\nclearTask.clearColor = true;\nclearTask.clearDepth = true;\nclearTask.targetTexture = colorTexture;\nclearTask.depthTexture = depthTexture;\n\nframeGraph.addTask(clearTask);\n\nconst rlist = {\n meshes: scene.meshes,\n particleSystems: scene.particleSystems,\n}\n\nconst renderTask = new BABYLON.FrameGraphObjectRendererTask(\"renderObjects\", frameGraph, scene);\n\nrenderTask.targetTexture = clearTask.outputTexture;\nrenderTask.depthTexture = clearTask.outputDepthTexture;\nrenderTask.objectList = rlist;\nrenderTask.camera = camera;\n\nframeGraph.addTask(renderTask);\n\nconst copyToBackbufferTask = new BABYLON.FrameGraphCopyToBackbufferColorTask(\"copytobackbuffer\", frameGraph);\n\ncopyToBackbufferTask.sourceTexture = renderTask.outputTexture;\n\nframeGraph.addTask(copyToBackbufferTask);\n\nframeGraph.build();\n\nframeGraph.pausedExecution = true;\nawait frameGraph.whenReadyAsync();\nframeGraph.pausedExecution = false;" }, { "label": "Frame Graph: simple node render graph", From b1e3ce2e78c0e4bd59c0eb441638ab29f3149806 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 15:47:20 +0100 Subject: [PATCH 12/17] Add missing properties --- .../FrameGraph/Node/Blocks/executeBlock.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/executeBlock.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/executeBlock.ts index fb1446b5ce8..b3f260ac8b3 100644 --- a/packages/dev/core/src/FrameGraph/Node/Blocks/executeBlock.ts +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/executeBlock.ts @@ -1,4 +1,4 @@ -import type { NodeRenderGraphConnectionPoint, Scene, FrameGraph } from "core/index"; +import type { NodeRenderGraphConnectionPoint, Scene, FrameGraph, FrameGraphContext } from "core/index"; import { RegisterClass } from "../../../Misc/typeStore"; import { NodeRenderGraphBlockConnectionPointTypes } from "../Types/nodeRenderGraphTypes"; import { NodeRenderGraphBlock } from "../nodeRenderGraphBlock"; @@ -35,6 +35,28 @@ export class NodeRenderGraphExecuteBlock extends NodeRenderGraphBlock { this._frameGraphTask = new FrameGraphExecuteTask(name, frameGraph); } + /** + * Gets or sets the execute function + */ + public get func(): (context: FrameGraphContext) => void { + return this._frameGraphTask.func; + } + + public set func(func: (context: FrameGraphContext) => void) { + this._frameGraphTask.func = func; + } + + /** + * Gets or sets the execute when task disabled function + */ + public get funcDisabled(): ((context: FrameGraphContext) => void) | undefined { + return this._frameGraphTask.funcDisabled; + } + + public set funcDisabled(func: ((context: FrameGraphContext) => void) | undefined) { + this._frameGraphTask.funcDisabled = func; + } + /** * Gets the current class name * @returns the class name From 59f75274eb192ca9e6bf7e2df5ab46f99e0efac4 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Sun, 2 Nov 2025 15:53:16 +0100 Subject: [PATCH 13/17] Fix ignoreCameraMaxZ --- packages/dev/core/src/Cameras/camera.ts | 38 ++++++++++++++++--------- packages/dev/core/src/Meshes/mesh.ts | 2 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/dev/core/src/Cameras/camera.ts b/packages/dev/core/src/Cameras/camera.ts index 3b853928cc1..9a2e07d9f43 100644 --- a/packages/dev/core/src/Cameras/camera.ts +++ b/packages/dev/core/src/Cameras/camera.ts @@ -365,6 +365,12 @@ export class Camera extends Node { @serialize() public isStereoscopicSideBySide: boolean; + /** + * Ignores the cameraZ when computing the projection matrix (ie. use 0 instead of cameraZ), meaning objects won't be culled by the far plane + */ + @serialize() + public ignoreCameraMaxZ = false; + /** * Defines the list of custom render target which are rendered to and then used as the input to this camera's render. Eg. display another camera view on a TV in the main scene * This is pretty helpful if you wish to make a camera render to a texture you could reuse somewhere @@ -653,7 +659,9 @@ export class Camera extends Node { /** @internal */ public _isSynchronizedProjectionMatrix(): boolean { - let isSynchronized = this._cache.mode === this.mode && this._cache.minZ === this.minZ && this._cache.maxZ === this.maxZ; + const maxZ = this.ignoreCameraMaxZ ? 0 : this.maxZ; + + let isSynchronized = this._cache.mode === this.mode && this._cache.minZ === this.minZ && this._cache.maxZ === maxZ; if (!isSynchronized) { return false; @@ -933,10 +941,12 @@ export class Camera extends Node { return this._projectionMatrix; } + const maxZ = this.ignoreCameraMaxZ ? 0 : this.maxZ; + // Cache this._cache.mode = this.mode; this._cache.minZ = this.minZ; - this._cache.maxZ = this.maxZ; + this._cache.maxZ = maxZ; // Matrix this._refreshFrustumPlanes = true; @@ -974,8 +984,8 @@ export class Camera extends Node { getProjectionMatrix( this.fov, engine.getAspectRatio(this), - reverseDepth ? this.maxZ : this.minZ, - reverseDepth ? this.minZ : this.maxZ, + reverseDepth ? maxZ : this.minZ, + reverseDepth ? this.minZ : maxZ, this._projectionMatrix, this.fovMode === Camera.FOVMODE_VERTICAL_FIXED, engine.isNDCHalfZRange, @@ -992,8 +1002,8 @@ export class Camera extends Node { this.orthoRight ?? halfWidth, this.orthoBottom ?? -halfHeight, this.orthoTop ?? halfHeight, - reverseDepth ? this.maxZ : this.minZ, - reverseDepth ? this.minZ : this.maxZ, + reverseDepth ? maxZ : this.minZ, + reverseDepth ? this.minZ : maxZ, this.oblique.length, this.oblique.angle, this._computeObliqueDistance(this.oblique.offset), @@ -1006,8 +1016,8 @@ export class Camera extends Node { this.orthoRight ?? halfWidth, this.orthoBottom ?? -halfHeight, this.orthoTop ?? halfHeight, - reverseDepth ? this.maxZ : this.minZ, - reverseDepth ? this.minZ : this.maxZ, + reverseDepth ? maxZ : this.minZ, + reverseDepth ? this.minZ : maxZ, this._projectionMatrix, engine.isNDCHalfZRange ); @@ -1019,8 +1029,8 @@ export class Camera extends Node { this.orthoRight ?? halfWidth, this.orthoBottom ?? -halfHeight, this.orthoTop ?? halfHeight, - reverseDepth ? this.maxZ : this.minZ, - reverseDepth ? this.minZ : this.maxZ, + reverseDepth ? maxZ : this.minZ, + reverseDepth ? this.minZ : maxZ, this.oblique.length, this.oblique.angle, this._computeObliqueDistance(this.oblique.offset), @@ -1033,8 +1043,8 @@ export class Camera extends Node { this.orthoRight ?? halfWidth, this.orthoBottom ?? -halfHeight, this.orthoTop ?? halfHeight, - reverseDepth ? this.maxZ : this.minZ, - reverseDepth ? this.minZ : this.maxZ, + reverseDepth ? maxZ : this.minZ, + reverseDepth ? this.minZ : maxZ, this._projectionMatrix, engine.isNDCHalfZRange ); @@ -1335,7 +1345,7 @@ export class Camera extends Node { this._cameraRigParams.vrMetrics.aspectRatioFov, this._cameraRigParams.vrMetrics.aspectRatio, this.minZ, - this.maxZ, + this.ignoreCameraMaxZ ? 0 : this.maxZ, this._cameraRigParams.vrWorkMatrix, true, this.getEngine().isNDCHalfZRange @@ -1374,7 +1384,7 @@ export class Camera extends Node { public _updateRigCameras() { for (let i = 0; i < this._rigCameras.length; i++) { this._rigCameras[i].minZ = this.minZ; - this._rigCameras[i].maxZ = this.maxZ; + this._rigCameras[i].maxZ = this.ignoreCameraMaxZ ? 0 : this.maxZ; this._rigCameras[i].fov = this.fov; this._rigCameras[i].upVector.copyFrom(this.upVector); } diff --git a/packages/dev/core/src/Meshes/mesh.ts b/packages/dev/core/src/Meshes/mesh.ts index e94041054f2..065c6eb0444 100644 --- a/packages/dev/core/src/Meshes/mesh.ts +++ b/packages/dev/core/src/Meshes/mesh.ts @@ -2590,7 +2590,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { const engine = scene.getEngine(); let oldCameraMaxZ = 0; let oldCamera: Nullable = null; - if (this.ignoreCameraMaxZ && scene.activeCamera) { + if (this.ignoreCameraMaxZ && scene.activeCamera && !scene._isInIntermediateRendering()) { oldCameraMaxZ = scene.activeCamera.maxZ; oldCamera = scene.activeCamera; scene.activeCamera.maxZ = 0; From 9b63999b748c5ef3d58fcf4b546a4742d9a319fe Mon Sep 17 00:00:00 2001 From: Popov72 Date: Mon, 3 Nov 2025 19:33:49 +0100 Subject: [PATCH 14/17] Fix doc --- packages/dev/core/src/Cameras/camera.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/core/src/Cameras/camera.ts b/packages/dev/core/src/Cameras/camera.ts index 9a2e07d9f43..edf277d814f 100644 --- a/packages/dev/core/src/Cameras/camera.ts +++ b/packages/dev/core/src/Cameras/camera.ts @@ -366,7 +366,7 @@ export class Camera extends Node { public isStereoscopicSideBySide: boolean; /** - * Ignores the cameraZ when computing the projection matrix (ie. use 0 instead of cameraZ), meaning objects won't be culled by the far plane + * Ignores camera maxZ when computing the projection matrix (ie. use 0 instead of maxZ), meaning objects won't be culled by the far plane */ @serialize() public ignoreCameraMaxZ = false; From cc375dd9ed5bdec0036ee5ec683af785abb5f0eb Mon Sep 17 00:00:00 2001 From: Popov72 Date: Tue, 4 Nov 2025 14:45:21 +0100 Subject: [PATCH 15/17] Add creation flags support to the input block --- .../src/FrameGraph/Node/Blocks/inputBlock.ts | 2 ++ .../properties/inputNodePropertyComponent.tsx | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/inputBlock.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/inputBlock.ts index e13cd3d41bc..21252f64d80 100644 --- a/packages/dev/core/src/FrameGraph/Node/Blocks/inputBlock.ts +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/inputBlock.ts @@ -90,6 +90,7 @@ export class NodeRenderGraphInputBlock extends NodeRenderGraphBlock { formats: [Constants.TEXTUREFORMAT_RGBA], samples: 1, useSRGBBuffers: [false], + creationFlags: [0], }, sizeIsPercentage: true, isHistoryTexture: false, @@ -105,6 +106,7 @@ export class NodeRenderGraphInputBlock extends NodeRenderGraphBlock { types: [Constants.TEXTURETYPE_UNSIGNED_BYTE], formats: [Constants.TEXTUREFORMAT_DEPTH24_STENCIL8], useSRGBBuffers: [false], + creationFlags: [0], labels: [this.name], samples: 1, }, diff --git a/packages/tools/nodeRenderGraphEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx b/packages/tools/nodeRenderGraphEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx index 5e7fb8ff4cf..c7be92619d3 100644 --- a/packages/tools/nodeRenderGraphEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx +++ b/packages/tools/nodeRenderGraphEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx @@ -14,6 +14,7 @@ import { TextLineComponent } from "shared-ui-components/lines/textLineComponent" import type { FrameGraphObjectList } from "core/FrameGraph/frameGraphObjectList"; import type { Camera } from "core/Cameras/camera"; import type { IShadowLight } from "core/Lights/shadowLight"; +import { Constants } from "core/Engines/constants"; export class InputPropertyTabComponent extends React.Component { private _onValueChangedObserver: Nullable>; @@ -47,6 +48,9 @@ export class InputPropertyTabComponent extends React.Component this.props.stateManager.onRebuildRequiredObservable.notifyObservers()} /> + { + creationOptions.options.creationFlags![0] = value ? Constants.TEXTURE_CREATIONFLAG_STORAGE : 0; + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + }} + isSelected={() => creationOptions.options.creationFlags![0] === Constants.TEXTURE_CREATIONFLAG_STORAGE} + /> creationOptions.options.useSRGBBuffers![0]} + isSelected={() => creationOptions.options.useSRGBBuffers![0]} /> creationOptions.isHistoryTexture!} + isSelected={() => creationOptions.isHistoryTexture!} /> )} From 5fe99e06ba758a57e3d211aa2e53d5fc9a91aa09 Mon Sep 17 00:00:00 2001 From: Popov72 Date: Tue, 4 Nov 2025 14:45:45 +0100 Subject: [PATCH 16/17] Add compute shader block to NRG --- .../Node/Blocks/computeShaderBlock.ts | 101 ++++++++++++++++++ .../core/src/FrameGraph/Node/Blocks/index.ts | 1 + .../src/FrameGraph/Node/Blocks/outputBlock.ts | 1 + .../FrameGraph/Node/nodeRenderGraphBlock.ts | 8 ++ .../Tasks/Misc/computeShaderTask.ts | 47 +++++--- .../nodeRenderGraphEditor/src/blockTools.ts | 4 + .../components/nodeList/nodeListComponent.tsx | 3 +- 7 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 packages/dev/core/src/FrameGraph/Node/Blocks/computeShaderBlock.ts diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/computeShaderBlock.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/computeShaderBlock.ts new file mode 100644 index 00000000000..864cab0c965 --- /dev/null +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/computeShaderBlock.ts @@ -0,0 +1,101 @@ +import type { NodeRenderGraphConnectionPoint, Scene, FrameGraph, IComputeShaderPath, IComputeShaderOptions } from "core/index"; +import { RegisterClass } from "../../../Misc/typeStore"; +import { NodeRenderGraphBlockConnectionPointTypes } from "../Types/nodeRenderGraphTypes"; +import { NodeRenderGraphBlock } from "../nodeRenderGraphBlock"; +import { FrameGraphComputeShaderTask } from "../../Tasks/Misc/computeShaderTask"; + +/** + * Block used to execute a compute shader in the frame graph + */ +export class NodeRenderGraphComputeShaderBlock extends NodeRenderGraphBlock { + protected override _frameGraphTask: FrameGraphComputeShaderTask; + + /** + * Gets the frame graph task associated with this block + */ + public override get task() { + return this._frameGraphTask; + } + + /** + * Creates a new NodeRenderGraphComputeShaderBlock + * @param name defines the block name + * @param frameGraph defines the hosting frame graph + * @param scene defines the hosting scene + * @param computeShaderPath defines the compute shader path or source + * @param computeShaderOptions defines the compute shader options + */ + public constructor( + name: string, + frameGraph: FrameGraph, + scene: Scene, + computeShaderPath: string | IComputeShaderPath = "@compute @workgroup_size(1, 1, 1)\nfn main() {}", + computeShaderOptions: IComputeShaderOptions = { bindingsMapping: {} } + ) { + super(name, frameGraph, scene); + + this._additionalConstructionParameters = [computeShaderPath, computeShaderOptions]; + + this._addDependenciesInput( + NodeRenderGraphBlockConnectionPointTypes.Camera | NodeRenderGraphBlockConnectionPointTypes.ShadowLight | NodeRenderGraphBlockConnectionPointTypes.ObjectList + ); + + this.registerOutput("output", NodeRenderGraphBlockConnectionPointTypes.ResourceContainer); + + this._frameGraphTask = new FrameGraphComputeShaderTask(name, frameGraph, computeShaderPath, computeShaderOptions); + } + + private _createTask(shaderPath: string | IComputeShaderPath, shaderOptions?: IComputeShaderOptions) { + const dispatchSize = this._frameGraphTask.dispatchSize; + const indirectDispatch = this._frameGraphTask.indirectDispatch; + const execute = this._frameGraphTask.execute; + + this._frameGraphTask.dispose(); + this._frameGraphTask = new FrameGraphComputeShaderTask(this.name, this._frameGraph, shaderPath, shaderOptions); + + this._frameGraphTask.dispatchSize = dispatchSize; + this._frameGraphTask.indirectDispatch = indirectDispatch; + this._frameGraphTask.execute = execute; + + this._additionalConstructionParameters = [shaderPath, shaderOptions]; + } + + /** + * Gets or sets the execute function + */ + public get shaderPath(): string | IComputeShaderPath { + return this._frameGraphTask.computeShader.shaderPath; + } + + public set shaderPath(path: string | IComputeShaderPath) { + this._createTask(path, this.shaderOptions); + } + + /** + * Gets or sets the execute when task disabled function + */ + public get shaderOptions(): IComputeShaderOptions { + return this._frameGraphTask.computeShader.options; + } + + public set shaderOptions(options: IComputeShaderOptions) { + this._createTask(this.shaderPath, options); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "NodeRenderGraphComputeShaderBlock"; + } + + /** + * Gets the output component + */ + public get output(): NodeRenderGraphConnectionPoint { + return this._outputs[0]; + } +} + +RegisterClass("BABYLON.NodeRenderGraphComputeShaderBlock", NodeRenderGraphComputeShaderBlock); diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/index.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/index.ts index 030cf1de2ca..556bb722bdf 100644 --- a/packages/dev/core/src/FrameGraph/Node/Blocks/index.ts +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/index.ts @@ -1,3 +1,4 @@ +export * from "./computeShaderBlock"; export * from "./cullObjectsBlock"; export * from "./elbowBlock"; export * from "./executeBlock"; diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/outputBlock.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/outputBlock.ts index ed21295a2b9..a2d6c33440c 100644 --- a/packages/dev/core/src/FrameGraph/Node/Blocks/outputBlock.ts +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/outputBlock.ts @@ -29,6 +29,7 @@ export class NodeRenderGraphOutputBlock extends NodeRenderGraphBlock { this._isUnique = true; this.registerInput("texture", NodeRenderGraphBlockConnectionPointTypes.Texture); + this._addDependenciesInput(); this.texture.addAcceptedConnectionPointTypes(NodeRenderGraphBlockConnectionPointTypes.TextureAll); diff --git a/packages/dev/core/src/FrameGraph/Node/nodeRenderGraphBlock.ts b/packages/dev/core/src/FrameGraph/Node/nodeRenderGraphBlock.ts index 1d420dbb66b..20c8a529e77 100644 --- a/packages/dev/core/src/FrameGraph/Node/nodeRenderGraphBlock.ts +++ b/packages/dev/core/src/FrameGraph/Node/nodeRenderGraphBlock.ts @@ -287,6 +287,14 @@ export class NodeRenderGraphBlock { const dependencies = this.getInputByName("dependencies")!; + Object.defineProperty(this, "dependencies", { + get: function (this: FrameGraphTask) { + return dependencies; + }, + enumerable: true, + configurable: true, + }); + dependencies.addExcludedConnectionPointFromAllowedTypes( NodeRenderGraphBlockConnectionPointTypes.TextureAllButBackBuffer | NodeRenderGraphBlockConnectionPointTypes.ResourceContainer | diff --git a/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts b/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts index ee5e56a6b69..117789d4034 100644 --- a/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts +++ b/packages/dev/core/src/FrameGraph/Tasks/Misc/computeShaderTask.ts @@ -16,11 +16,13 @@ import { FrameGraphTask } from "../../frameGraphTask"; import { ComputeShader } from "core/Compute/computeShader"; import { Vector3 } from "core/Maths/math.vector"; import { UniformBuffer } from "core/Materials/uniformBuffer"; +import { Logger } from "core/Misc/logger"; /** * Task used to execute a compute shader (WebGPU only) */ export class FrameGraphComputeShaderTask extends FrameGraphTask { + private readonly _notSupported: boolean; private readonly _cs: ComputeShader; private readonly _ubo: { [name: string]: { ubo: UniformBuffer; autoUpdate: boolean } }; @@ -71,12 +73,19 @@ export class FrameGraphComputeShaderTask extends FrameGraphTask { constructor(name: string, frameGraph: FrameGraph, shaderPath: IComputeShaderPath | string, options: Partial = {}) { super(name, frameGraph); + if (!frameGraph.engine.getCaps().supportComputeShaders) { + this._notSupported = true; + Logger.Error("This engine does not support compute shaders!"); + return; + } + + this._notSupported = false; this._cs = new ComputeShader(name + "_cs", frameGraph.engine, shaderPath, options); this._ubo = {}; } public override isReady(): boolean { - return this._cs.isReady(); + return this._notSupported ? true : this._cs.isReady(); } /** @@ -177,22 +186,30 @@ export class FrameGraphComputeShaderTask extends FrameGraphTask { public record(skipCreationOfDisabledPasses?: boolean): FrameGraphPass { const pass = this._frameGraph.addPass(this.name); - pass.setExecuteFunc((context) => { - this.execute?.(context); + if (this._notSupported) { + pass.setExecuteFunc(() => {}); + } else { + pass.setExecuteFunc((context) => { + this.execute?.(context); + + for (const key in this._ubo) { + const uboEntry = this._ubo[key]; + if (uboEntry.autoUpdate) { + uboEntry.ubo.update(); + } + } - for (const key in this._ubo) { - const uboEntry = this._ubo[key]; - if (uboEntry.autoUpdate) { - uboEntry.ubo.update(); + if (this.indirectDispatch) { + context.pushDebugGroup(`Indirect dispatch compute shader (${this.name})`); + this._cs.dispatchIndirect(this.indirectDispatch.buffer, this.indirectDispatch.offset); + context.popDebugGroup(); + } else { + context.pushDebugGroup(`Dispatch compute shader (${this.name})`); + this._cs.dispatch(this.dispatchSize.x, this.dispatchSize.y, this.dispatchSize.z); + context.popDebugGroup(); } - } - - if (this.indirectDispatch) { - this._cs.dispatchIndirect(this.indirectDispatch.buffer, this.indirectDispatch.offset); - } else { - this._cs.dispatch(this.dispatchSize.x, this.dispatchSize.y, this.dispatchSize.z); - } - }); + }); + } if (!skipCreationOfDisabledPasses) { const passDisabled = this._frameGraph.addPass(this.name + "_disabled", true); diff --git a/packages/tools/nodeRenderGraphEditor/src/blockTools.ts b/packages/tools/nodeRenderGraphEditor/src/blockTools.ts index 6ebee7c5eeb..a36a01c00b6 100644 --- a/packages/tools/nodeRenderGraphEditor/src/blockTools.ts +++ b/packages/tools/nodeRenderGraphEditor/src/blockTools.ts @@ -42,6 +42,7 @@ import { NodeRenderGraphColorCorrectionPostProcessBlock } from "core/FrameGraph/ import { NodeRenderGraphFilterPostProcessBlock } from "core/FrameGraph/Node/Blocks/PostProcesses/filterPostProcessBlock"; import { NodeRenderGraphTonemapPostProcessBlock } from "core/FrameGraph/Node/Blocks/PostProcesses/tonemapPostProcessBlock"; import { NodeRenderGraphSSAO2PostProcessBlock } from "core/FrameGraph/Node/Blocks/PostProcesses/ssao2PostProcessBlock"; +import { NodeRenderGraphComputeShaderBlock } from "core/FrameGraph/Node/Blocks/computeShaderBlock"; /** * Static class for BlockTools @@ -191,6 +192,9 @@ export class BlockTools { case "SSAO2Block": { return new NodeRenderGraphSSAO2PostProcessBlock("SSAO", frameGraph, scene); } + case "ComputeShaderBlock": { + return new NodeRenderGraphComputeShaderBlock("Compute Shader", frameGraph, scene); + } } return null; diff --git a/packages/tools/nodeRenderGraphEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeRenderGraphEditor/src/components/nodeList/nodeListComponent.tsx index eebe39093d1..2a8bc088079 100644 --- a/packages/tools/nodeRenderGraphEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeRenderGraphEditor/src/components/nodeList/nodeListComponent.tsx @@ -70,6 +70,7 @@ export class NodeListComponent extends React.Component Date: Tue, 4 Nov 2025 17:51:31 +0100 Subject: [PATCH 17/17] Misc minor updates --- .../Node/Blocks/PostProcesses/bloomPostProcessBlock.ts | 2 +- .../PostProcesses/chromaticAberrationPostProcessBlock.ts | 2 +- .../core/src/FrameGraph/Tasks/PostProcesses/bloomTask.ts | 2 +- .../dev/core/src/FrameGraph/frameGraphTextureManager.ts | 8 ++++++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/bloomPostProcessBlock.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/bloomPostProcessBlock.ts index fcc8462d5d3..2b6635b5e84 100644 --- a/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/bloomPostProcessBlock.ts +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/bloomPostProcessBlock.ts @@ -72,7 +72,7 @@ export class NodeRenderGraphBloomPostProcessBlock extends NodeRenderGraphBasePos } /** The luminance threshold to find bright areas of the image to bloom. */ - @editableInPropertyPage("Threshold", PropertyTypeForEdition.Float, "PROPERTIES", { min: 0, max: 2 }) + @editableInPropertyPage("Threshold", PropertyTypeForEdition.Float, "PROPERTIES", { min: 0, max: 1 }) public get threshold(): number { return this._frameGraphTask.bloom.threshold; } diff --git a/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/chromaticAberrationPostProcessBlock.ts b/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/chromaticAberrationPostProcessBlock.ts index d380cbe8753..a96470975e0 100644 --- a/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/chromaticAberrationPostProcessBlock.ts +++ b/packages/dev/core/src/FrameGraph/Node/Blocks/PostProcesses/chromaticAberrationPostProcessBlock.ts @@ -44,7 +44,7 @@ export class NodeRenderGraphChromaticAberrationPostProcessBlock extends NodeRend } /** The amount the effect will increase for pixels closer to the edge of the screen */ - @editableInPropertyPage("Radial intensity", PropertyTypeForEdition.Float, "PROPERTIES", { min: 0.1, max: 5 }) + @editableInPropertyPage("Radial intensity", PropertyTypeForEdition.Float, "PROPERTIES", { min: -1, max: 5 }) public get radialIntensity(): number { return this._frameGraphTask.postProcess.radialIntensity; } diff --git a/packages/dev/core/src/FrameGraph/Tasks/PostProcesses/bloomTask.ts b/packages/dev/core/src/FrameGraph/Tasks/PostProcesses/bloomTask.ts index 561e70d0612..48dc79f1d37 100644 --- a/packages/dev/core/src/FrameGraph/Tasks/PostProcesses/bloomTask.ts +++ b/packages/dev/core/src/FrameGraph/Tasks/PostProcesses/bloomTask.ts @@ -84,7 +84,7 @@ export class FrameGraphBloomTask extends FrameGraphTask { * @param hdr Whether the bloom effect is HDR. * @param bloomScale The scale of the bloom effect. This value is multiplied by the source texture size to determine the bloom texture size. */ - constructor(name: string, frameGraph: FrameGraph, weight: number, kernel: number, threshold: number, hdr = false, bloomScale = 0.5) { + constructor(name: string, frameGraph: FrameGraph, weight = 0.25, kernel = 64, threshold = 0.2, hdr = false, bloomScale = 0.5) { super(name, frameGraph); this.hdr = hdr; diff --git a/packages/dev/core/src/FrameGraph/frameGraphTextureManager.ts b/packages/dev/core/src/FrameGraph/frameGraphTextureManager.ts index 431b34fc4ba..8a00177184e 100644 --- a/packages/dev/core/src/FrameGraph/frameGraphTextureManager.ts +++ b/packages/dev/core/src/FrameGraph/frameGraphTextureManager.ts @@ -1051,14 +1051,18 @@ export class FrameGraphTextureManager { } let textureEntry = this._textures.get(textureHandle); if (!textureEntry) { - throw new Error(`FrameGraph._computeTextureLifespan: Texture handle "${textureHandle}" not found in the texture manager.`); + throw new Error( + `FrameGraph._computeTextureLifespan: Texture handle "${textureHandle}" not found in the texture manager. Make sure you didn't forget to add a task in the frame graph.` + ); } let handle = textureHandle; while (textureEntry.refHandle !== undefined) { handle = textureEntry.refHandle; textureEntry = this._textures.get(handle); if (!textureEntry) { - throw new Error(`FrameGraph._computeTextureLifespan: Texture handle "${handle}" not found in the texture manager (source handle="${textureHandle}").`); + throw new Error( + `FrameGraph._computeTextureLifespan: Texture handle "${handle}" not found in the texture manager (source handle="${textureHandle}"). Make sure you didn't forget to add a task in the frame graph.` + ); } } if (textureEntry.namespace === FrameGraphTextureNamespace.External || this._historyTextures.has(handle)) {