Frame Graph replacing the scene render loop
Using the frame graph classes directly
Start by creating the frame graph:
import { FrameGraph } from "@babylonjs/core/FrameGraph/frameGraph";const frameGraph = new FrameGraph(scene, true);scene.frameGraph = frameGraph;scene.cameraToUseForPointers = camera;The second parameter of the FrameGraph constructor instructs the framework to create debugging textures for the textures created by the frame graph, which you can browse in the inspector. This can help you debug and understand what's going on in your code!
We set scene.frameGraph = frameGraph so that the frame graph is used instead of the usual scene render loop.
Let's create a color and depth texture, which will be used to render the meshes:
import { Constants } from "@babylonjs/core/Engines/constants";const colorTexture = frameGraph.textureManager.createRenderTargetTexture("color", { size: { width: 100, height: 100 }, options: { createMipMaps: false, types: [Constants.TEXTURETYPE_UNSIGNED_BYTE], formats: [Constants.TEXTUREFORMAT_RGBA], samples: 4, useSRGBBuffers: [false], labels: ["color"], }, sizeIsPercentage: true,});const depthTexture = frameGraph.textureManager.createRenderTargetTexture("depth", { size: { width: 100, height: 100 }, options: { createMipMaps: false, types: [Constants.TEXTURETYPE_UNSIGNED_BYTE], formats: [Constants.TEXTUREFORMAT_DEPTH32_FLOAT], samples: 4, useSRGBBuffers: [false], labels: ["depth"], }, sizeIsPercentage: true,});It's pretty straightforward code.
You can see that we create the textures through the frameGraph.textureManager object. It is important to create the textures used by the frame graph in this way and not by doing new RenderTargetTexture(...) or engine.createRenderTargetTexture(...) because the textures used by a frame graph are managed in a specific way.
This is why frameGraph.textureManager.createRenderTargetTexture returns a texture handle (a number), not a real texture object: most frame graph framework methods that deal with textures use texture handles!
Refer to Introduction to Frame Graph classes for more information about texture handles.
Note that you can import an existing texture into a frame graph by calling frameGraph.textureManager.importTexture(), and this method will return a texture handle that you can use as input for frame graph tasks. There is also the inverse method frameGraph.textureManager.getTextureFromHandle(), which allows you to obtain the real texture object from a texture handle (useful when you use a frame graph at the same time as the scene render loop - see the following section for more information).
An important property of the object passed to the createRenderTargetTexture method is sizeIsPercentage: if it is true, it means that the size values are percentages instead of fixed pixel sizes. These percentages are related to the size of the output screen. If you set width=height=100, the texture will be created with the same size as the output screen. If you set these values to 50, the texture will be created at half the size of the screen. Most of the time, you will want to set sizeIsPercentage=true to keep your frame graph independent of the output size.
We will now create a task that clears the textures:
import { FrameGraphClearTextureTask } from "@babylonjs/core/FrameGraph/Tasks/Texture/clearTextureTask";const clearTask = new FrameGraphClearTextureTask("clear", frameGraph);clearTask.clearColor = true;clearTask.clearDepth = true;clearTask.targetTexture = colorTexture;clearTask.depthTexture = depthTexture;frameGraph.addTask(clearTask);This code creates a "clear" task, configures it and adds it to the frame graph.
Refer to Frame Graph Task List for detailed explanations of the various frame graph tasks used in the code snippets on this page.
The main task is the rendering of the meshes:
import { FrameGraphObjectRendererTask } from "@babylonjs/core/FrameGraph/Tasks/Rendering/objectRendererTask";const rlist = { meshes: scene.meshes, particleSystems: []};const renderTask = new FrameGraphObjectRendererTask("renderObjects", frameGraph, scene);renderTask.targetTexture = clearTask.outputTexture;renderTask.depthTexture = clearTask.outputDepthTexture;renderTask.objectList = rlist;renderTask.camera = camera;frameGraph.addTask(renderTask);Once again, the code is fairly simple. We create the appropriate task (FrameGraphObjectRendererTask), configure it, and add it to the frame graph.
Notice how the previous task is linked to this task: the color/depth output texture of the “clear” task is defined as the target/depth input texture of the “renderObjects” task.
The last task simply copies the texture to the output:
import { FrameGraphCopyToBackbufferColorTask } from "@babylonjs/core/FrameGraph/Tasks/Texture/copyToBackbufferColorTask";const copyToBackbufferTask = new FrameGraphCopyToBackbufferColorTask("copytobackbuffer", frameGraph);copyToBackbufferTask.sourceTexture = renderTask.outputTexture;frameGraph.addTask(copyToBackbufferTask);Once all tasks have been added to the frame graph, you must build it by calling await FrameGraph.buildAsync(). This creates the various passes that will be executed when FrameGraph.execute() is called and ensures that everything is ready before the graph can be executed (among other things, it allocates textures).
Finally, don't forget to handle screen resizing:
engine.onResizeObservable.add(async () => { await frameGraph.buildAsync();});
await frameGraph.buildAsync();Here's the PG corresponding to this example: Frame Graph basic example
Using a node render graph
Let's do the same thing, but this time using a node render graph created with the Node Render Graph Editor.
Here is the node render graph: https://nrge.babylonjs.com/#CCDXLX#3 (this is the default graph you get when you browse to the NRGE URL)
The JavaScript code:
import { NodeRenderGraph } from "@babylonjs/core/FrameGraph/Node/nodeRenderGraph";const nrg = await NodeRenderGraph.ParseFromSnippetAsync("#CCDXLX", scene);await nrg.buildAsync();That's all you need to make it work with a node render graph!
The full PG: Node Render Graph basic example
By default, calling nrg.buildAsync() will also assign the frame graph to scene.frameGraph.
For more complicated examples, you may need to pass a third parameter to NodeRenderGraph.ParseFromSnippetAsync() to configure the node render graph:
import { NodeRenderGraph } from "@babylonjs/core/FrameGraph/Node/nodeRenderGraph";const nrg = await NodeRenderGraph.ParseFromSnippetAsync("#CCDXLX", scene, { debugTextures: false, autoConfigure: false, verbose: false, rebuildGraphOnEngineResize: true, autoFillExternalInputs: true,});The code snippet above shows you the default values of the various parameters.
autoFillExternalInputs:true will try to give reasonable values to certain input resources:
cameraresources will be set toscene.cameras[0]for the first camera resource,scene.cameras[1]for the second camera resource, and so on.object listresources will be set toscene.meshesandscene.particleSystems.shadow lightresources will be set to the first shadow light ofscene.lightsfor the first light resource, to the second shadow light ofscene.lightsfor the second light resource, and so on.